Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / third_party / WebKit / Source / devtools / front_end / ui / treeoutline.js
1 /*
2  * Copyright (C) 2007 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer.
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution.
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 /**
30  * @constructor
31  * @param {!Element} listNode
32  * @param {boolean=} nonFocusable
33  */
34 function TreeOutline(listNode, nonFocusable)
35 {
36     /** @type {!Array.<!TreeElement>} */
37     this.children = [];
38     this.selectedTreeElement = null;
39     this._childrenListNode = listNode;
40     this.childrenListElement = this._childrenListNode;
41     this._childrenListNode.removeChildren();
42     this.expandTreeElementsWhenArrowing = false;
43     this.root = true;
44     this.hasChildren = false;
45     this.expanded = true;
46     this.selected = false;
47     this.treeOutline = this;
48     /** @type {?function(!TreeElement, !TreeElement):number} */
49     this.comparator = null;
50
51     this.setFocusable(!nonFocusable);
52     this._childrenListNode.addEventListener("keydown", this._treeKeyDown.bind(this), true);
53
54     /** @type {!Map.<!Object, !Array.<!TreeElement>>} */
55     this._treeElementsMap = new Map();
56     /** @type {!Map.<!Object, boolean>} */
57     this._expandedStateMap = new Map();
58     this.element = listNode;
59 }
60
61 TreeOutline.prototype.setFocusable = function(focusable)
62 {
63     if (focusable)
64         this._childrenListNode.setAttribute("tabIndex", 0);
65     else
66         this._childrenListNode.removeAttribute("tabIndex");
67 }
68
69 /**
70  * @param {!TreeElement} child
71  */
72 TreeOutline.prototype.appendChild = function(child)
73 {
74     var insertionIndex;
75     if (this.treeOutline.comparator)
76         insertionIndex = insertionIndexForObjectInListSortedByFunction(child, this.children, this.treeOutline.comparator);
77     else
78         insertionIndex = this.children.length;
79     this.insertChild(child, insertionIndex);
80 }
81
82 /**
83  * @param {!TreeElement} child
84  * @param {!TreeElement} beforeChild
85  */
86 TreeOutline.prototype.insertBeforeChild = function(child, beforeChild)
87 {
88     if (!child)
89         throw("child can't be undefined or null");
90
91     if (!beforeChild)
92         throw("beforeChild can't be undefined or null");
93
94     var childIndex = this.children.indexOf(beforeChild);
95     if (childIndex === -1)
96         throw("beforeChild not found in this node's children");
97
98     this.insertChild(child, childIndex);
99 }
100
101 /**
102  * @param {!TreeElement} child
103  * @param {number} index
104  */
105 TreeOutline.prototype.insertChild = function(child, index)
106 {
107     if (!child)
108         throw("child can't be undefined or null");
109
110     var previousChild = (index > 0 ? this.children[index - 1] : null);
111     if (previousChild) {
112         previousChild.nextSibling = child;
113         child.previousSibling = previousChild;
114     } else {
115         child.previousSibling = null;
116     }
117
118     var nextChild = this.children[index];
119     if (nextChild) {
120         nextChild.previousSibling = child;
121         child.nextSibling = nextChild;
122     } else {
123         child.nextSibling = null;
124     }
125
126     this.children.splice(index, 0, child);
127     this.hasChildren = true;
128     child.parent = this;
129     child.treeOutline = this.treeOutline;
130     child.treeOutline._rememberTreeElement(child);
131
132     var current = child.children[0];
133     while (current) {
134         current.treeOutline = this.treeOutline;
135         current.treeOutline._rememberTreeElement(current);
136         current = current.traverseNextTreeElement(false, child, true);
137     }
138
139     if (child.hasChildren && typeof(child.treeOutline._expandedStateMap.get(child.representedObject)) !== "undefined")
140         child.expanded = child.treeOutline._expandedStateMap.get(child.representedObject);
141
142     if (!this._childrenListNode) {
143         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
144         this._childrenListNode.parentTreeElement = this;
145         this._childrenListNode.classList.add("children");
146         if (this.hidden)
147             this._childrenListNode.classList.add("hidden");
148     }
149
150     child._attach();
151 }
152
153 /**
154  * @param {number} childIndex
155  */
156 TreeOutline.prototype.removeChildAtIndex = function(childIndex)
157 {
158     if (childIndex < 0 || childIndex >= this.children.length)
159         throw("childIndex out of range");
160
161     var child = this.children[childIndex];
162     this.children.splice(childIndex, 1);
163
164     var parent = child.parent;
165     if (child.deselect()) {
166         if (child.previousSibling)
167             child.previousSibling.select();
168         else if (child.nextSibling)
169             child.nextSibling.select();
170         else
171             parent.select();
172     }
173
174     if (child.previousSibling)
175         child.previousSibling.nextSibling = child.nextSibling;
176     if (child.nextSibling)
177         child.nextSibling.previousSibling = child.previousSibling;
178
179     if (child.treeOutline) {
180         child.treeOutline._forgetTreeElement(child);
181         child.treeOutline._forgetChildrenRecursive(child);
182     }
183
184     child._detach();
185     child.treeOutline = null;
186     child.parent = null;
187     child.nextSibling = null;
188     child.previousSibling = null;
189 }
190
191 /**
192  * @param {!TreeElement} child
193  */
194 TreeOutline.prototype.removeChild = function(child)
195 {
196     if (!child)
197         throw("child can't be undefined or null");
198
199     var childIndex = this.children.indexOf(child);
200     if (childIndex === -1)
201         throw("child not found in this node's children");
202
203     this.removeChildAtIndex.call(this, childIndex);
204 }
205
206 TreeOutline.prototype.removeChildren = function()
207 {
208     for (var i = 0; i < this.children.length; ++i) {
209         var child = this.children[i];
210         child.deselect();
211
212         if (child.treeOutline) {
213             child.treeOutline._forgetTreeElement(child);
214             child.treeOutline._forgetChildrenRecursive(child);
215         }
216
217         child._detach();
218         child.treeOutline = null;
219         child.parent = null;
220         child.nextSibling = null;
221         child.previousSibling = null;
222     }
223
224     this.children = [];
225 }
226
227 /**
228  * @param {!TreeElement} element
229  */
230 TreeOutline.prototype._rememberTreeElement = function(element)
231 {
232     if (!this._treeElementsMap.get(element.representedObject))
233         this._treeElementsMap.put(element.representedObject, []);
234
235     // check if the element is already known
236     var elements = this._treeElementsMap.get(element.representedObject);
237     if (elements.indexOf(element) !== -1)
238         return;
239
240     // add the element
241     elements.push(element);
242 }
243
244 /**
245  * @param {!TreeElement} element
246  */
247 TreeOutline.prototype._forgetTreeElement = function(element)
248 {
249     if (this._treeElementsMap.get(element.representedObject)) {
250         var elements = this._treeElementsMap.get(element.representedObject);
251         elements.remove(element, true);
252         if (!elements.length)
253             this._treeElementsMap.remove(element.representedObject);
254     }
255 }
256
257 /**
258  * @param {!TreeElement} parentElement
259  */
260 TreeOutline.prototype._forgetChildrenRecursive = function(parentElement)
261 {
262     var child = parentElement.children[0];
263     while (child) {
264         this._forgetTreeElement(child);
265         child = child.traverseNextTreeElement(false, parentElement, true);
266     }
267 }
268
269 /**
270  * @param {?Object} representedObject
271  * @return {?TreeElement}
272  */
273 TreeOutline.prototype.getCachedTreeElement = function(representedObject)
274 {
275     if (!representedObject)
276         return null;
277
278     var elements = this._treeElementsMap.get(representedObject);
279     if (elements && elements.length)
280         return elements[0];
281     return null;
282 }
283
284 /**
285  * @param {?Object} representedObject
286  * @param {function(!Object):?Object} getParent
287  * @return {?TreeElement}
288  */
289 TreeOutline.prototype.findTreeElement = function(representedObject, getParent)
290 {
291     if (!representedObject)
292         return null;
293
294     var cachedElement = this.getCachedTreeElement(representedObject);
295     if (cachedElement)
296         return cachedElement;
297
298     // Walk up the parent pointers from the desired representedObject
299     var ancestors = [];
300     for (var currentObject = getParent(representedObject); currentObject;  currentObject = getParent(currentObject)) {
301         ancestors.push(currentObject);
302         if (this.getCachedTreeElement(currentObject))  // stop climbing as soon as we hit
303             break;
304     }
305
306     if (!currentObject)
307         return null;
308
309     // Walk down to populate each ancestor's children, to fill in the tree and the cache.
310     for (var i = ancestors.length - 1; i >= 0; --i) {
311         var treeElement = this.getCachedTreeElement(ancestors[i]);
312         if (treeElement)
313             treeElement.onpopulate();  // fill the cache with the children of treeElement
314     }
315
316     return this.getCachedTreeElement(representedObject);
317 }
318
319 /**
320  * @param {number} x
321  * @param {number} y
322  * @return {?TreeElement}
323  */
324 TreeOutline.prototype.treeElementFromPoint = function(x, y)
325 {
326     var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y);
327     if (!node)
328         return null;
329
330     var listNode = node.enclosingNodeOrSelfWithNodeNameInArray(["ol", "li"]);
331     if (listNode)
332         return listNode.parentTreeElement || listNode.treeElement;
333     return null;
334 }
335
336 TreeOutline.prototype._treeKeyDown = function(event)
337 {
338     if (event.target !== this._childrenListNode)
339         return;
340
341     if (!this.selectedTreeElement || event.shiftKey || event.metaKey || event.ctrlKey)
342         return;
343
344     var handled = false;
345     var nextSelectedElement;
346     if (event.keyIdentifier === "Up" && !event.altKey) {
347         nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true);
348         while (nextSelectedElement && !nextSelectedElement.selectable)
349             nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(!this.expandTreeElementsWhenArrowing);
350         handled = nextSelectedElement ? true : false;
351     } else if (event.keyIdentifier === "Down" && !event.altKey) {
352         nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true);
353         while (nextSelectedElement && !nextSelectedElement.selectable)
354             nextSelectedElement = nextSelectedElement.traverseNextTreeElement(!this.expandTreeElementsWhenArrowing);
355         handled = nextSelectedElement ? true : false;
356     } else if (event.keyIdentifier === "Left") {
357         if (this.selectedTreeElement.expanded) {
358             if (event.altKey)
359                 this.selectedTreeElement.collapseRecursively();
360             else
361                 this.selectedTreeElement.collapse();
362             handled = true;
363         } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) {
364             handled = true;
365             if (this.selectedTreeElement.parent.selectable) {
366                 nextSelectedElement = this.selectedTreeElement.parent;
367                 while (nextSelectedElement && !nextSelectedElement.selectable)
368                     nextSelectedElement = nextSelectedElement.parent;
369                 handled = nextSelectedElement ? true : false;
370             } else if (this.selectedTreeElement.parent)
371                 this.selectedTreeElement.parent.collapse();
372         }
373     } else if (event.keyIdentifier === "Right") {
374         if (!this.selectedTreeElement.revealed()) {
375             this.selectedTreeElement.reveal();
376             handled = true;
377         } else if (this.selectedTreeElement.hasChildren) {
378             handled = true;
379             if (this.selectedTreeElement.expanded) {
380                 nextSelectedElement = this.selectedTreeElement.children[0];
381                 while (nextSelectedElement && !nextSelectedElement.selectable)
382                     nextSelectedElement = nextSelectedElement.nextSibling;
383                 handled = nextSelectedElement ? true : false;
384             } else {
385                 if (event.altKey)
386                     this.selectedTreeElement.expandRecursively();
387                 else
388                     this.selectedTreeElement.expand();
389             }
390         }
391     } else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */)
392         handled = this.selectedTreeElement.ondelete();
393     else if (isEnterKey(event))
394         handled = this.selectedTreeElement.onenter();
395     else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Space.code)
396         handled = this.selectedTreeElement.onspace();
397
398     if (nextSelectedElement) {
399         nextSelectedElement.reveal();
400         nextSelectedElement.select(false, true);
401     }
402
403     if (handled)
404         event.consume(true);
405 }
406
407 TreeOutline.prototype.expand = function()
408 {
409     // this is the root, do nothing
410 }
411
412 TreeOutline.prototype.collapse = function()
413 {
414     // this is the root, do nothing
415 }
416
417 /**
418  * @return {boolean}
419  */
420 TreeOutline.prototype.revealed = function()
421 {
422     return true;
423 }
424
425 TreeOutline.prototype.reveal = function()
426 {
427     // this is the root, do nothing
428 }
429
430 TreeOutline.prototype.select = function()
431 {
432     // this is the root, do nothing
433 }
434
435 /**
436  * @param {boolean=} omitFocus
437  */
438 TreeOutline.prototype.revealAndSelect = function(omitFocus)
439 {
440     // this is the root, do nothing
441 }
442
443 /**
444  * @constructor
445  * @param {string|!Node} title
446  * @param {?Object=} representedObject
447  * @param {boolean=} hasChildren
448  */
449 function TreeElement(title, representedObject, hasChildren)
450 {
451     this._title = title;
452     this.representedObject = (representedObject || {});
453
454     this.root = false;
455     this._hidden = false;
456     this._selectable = true;
457     this.expanded = false;
458     this.selected = false;
459     this.hasChildren = hasChildren;
460     this.children = [];
461     this.treeOutline = null;
462     this.parent = null;
463     this.previousSibling = null;
464     this.nextSibling = null;
465     this._listItemNode = null;
466 }
467
468 TreeElement.prototype = {
469     arrowToggleWidth: 10,
470
471     get selectable() {
472         if (this._hidden)
473             return false;
474         return this._selectable;
475     },
476
477     set selectable(x) {
478         this._selectable = x;
479     },
480
481     get listItemElement() {
482         return this._listItemNode;
483     },
484
485     get childrenListElement() {
486         return this._childrenListNode;
487     },
488
489     get title() {
490         return this._title;
491     },
492
493     set title(x) {
494         this._title = x;
495         this._setListItemNodeContent();
496     },
497
498     get tooltip() {
499         return this._tooltip;
500     },
501
502     set tooltip(x) {
503         this._tooltip = x;
504         if (this._listItemNode)
505             this._listItemNode.title = x ? x : "";
506     },
507
508     get hasChildren() {
509         return this._hasChildren;
510     },
511
512     set hasChildren(x) {
513         if (this._hasChildren === x)
514             return;
515
516         this._hasChildren = x;
517
518         if (!this._listItemNode)
519             return;
520
521         if (x)
522             this._listItemNode.classList.add("parent");
523         else {
524             this._listItemNode.classList.remove("parent");
525             this.collapse();
526         }
527     },
528
529     get hidden() {
530         return this._hidden;
531     },
532
533     set hidden(x) {
534         if (this._hidden === x)
535             return;
536
537         this._hidden = x;
538
539         if (x) {
540             if (this._listItemNode)
541                 this._listItemNode.classList.add("hidden");
542             if (this._childrenListNode)
543                 this._childrenListNode.classList.add("hidden");
544         } else {
545             if (this._listItemNode)
546                 this._listItemNode.classList.remove("hidden");
547             if (this._childrenListNode)
548                 this._childrenListNode.classList.remove("hidden");
549         }
550     },
551
552     get shouldRefreshChildren() {
553         return this._shouldRefreshChildren;
554     },
555
556     set shouldRefreshChildren(x) {
557         this._shouldRefreshChildren = x;
558         if (x && this.expanded)
559             this.expand();
560     },
561
562     _setListItemNodeContent: function()
563     {
564         if (!this._listItemNode)
565             return;
566
567         if (typeof this._title === "string")
568             this._listItemNode.textContent = this._title;
569         else {
570             this._listItemNode.removeChildren();
571             if (this._title)
572                 this._listItemNode.appendChild(this._title);
573         }
574     }
575 }
576
577 TreeElement.prototype.appendChild = TreeOutline.prototype.appendChild;
578 TreeElement.prototype.insertChild = TreeOutline.prototype.insertChild;
579 TreeElement.prototype.insertBeforeChild = TreeOutline.prototype.insertBeforeChild;
580 TreeElement.prototype.removeChild = TreeOutline.prototype.removeChild;
581 TreeElement.prototype.removeChildAtIndex = TreeOutline.prototype.removeChildAtIndex;
582 TreeElement.prototype.removeChildren = TreeOutline.prototype.removeChildren;
583
584 TreeElement.prototype._attach = function()
585 {
586     if (!this._listItemNode || this.parent._shouldRefreshChildren) {
587         if (this._listItemNode && this._listItemNode.parentNode)
588             this._listItemNode.parentNode.removeChild(this._listItemNode);
589
590         this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
591         this._listItemNode.treeElement = this;
592         this._setListItemNodeContent();
593         this._listItemNode.title = this._tooltip ? this._tooltip : "";
594
595         if (this.hidden)
596             this._listItemNode.classList.add("hidden");
597         if (this.hasChildren)
598             this._listItemNode.classList.add("parent");
599         if (this.expanded)
600             this._listItemNode.classList.add("expanded");
601         if (this.selected)
602             this._listItemNode.classList.add("selected");
603
604         this._listItemNode.addEventListener("mousedown", TreeElement.treeElementMouseDown, false);
605         this._listItemNode.addEventListener("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
624 TreeElement.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
632 TreeElement.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
648 TreeElement.treeElementSelectStart = function(event)
649 {
650     var element = event.currentTarget;
651     if (!element)
652         return;
653     element._selectionStarted = true;
654 }
655
656 TreeElement.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
693 TreeElement.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
706 TreeElement.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.put(this.representedObject, false);
717
718     this.oncollapse();
719 }
720
721 TreeElement.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
731 TreeElement.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.put(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
775 TreeElement.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  */
799 TreeElement.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
813 TreeElement.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  */
828 TreeElement.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
840 TreeElement.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  */
851 TreeElement.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  */
877 TreeElement.prototype.revealAndSelect = function(omitFocus)
878 {
879     this.reveal();
880     this.select(omitFocus);
881 }
882
883 /**
884  * @param {boolean=} supressOnDeselect
885  * @return {boolean}
886  */
887 TreeElement.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.
900 TreeElement.prototype.onpopulate = function() { }
901
902 /**
903  * @return {boolean}
904  */
905 TreeElement.prototype.onenter = function() { return false; }
906
907 /**
908  * @return {boolean}
909  */
910 TreeElement.prototype.ondelete = function() { return false; }
911
912 /**
913  * @return {boolean}
914  */
915 TreeElement.prototype.onspace = function() { return false; }
916
917 TreeElement.prototype.onattach = function() { }
918
919 TreeElement.prototype.onexpand = function() { }
920
921 TreeElement.prototype.oncollapse = function() { }
922
923 /**
924  * @param {!MouseEvent} e
925  * @return {boolean}
926  */
927 TreeElement.prototype.ondblclick = function(e) { return false; }
928
929 TreeElement.prototype.onreveal = function() { }
930
931 /**
932  * @param {boolean=} selectedByUser
933  * @return {boolean}
934  */
935 TreeElement.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  */
944 TreeElement.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  */
984 TreeElement.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  */
1008 TreeElement.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 }