REGRESSION: Web Inspector: Exception showing the DOM tree for a node with too many...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / TreeOutline.js
1 /*
2  * Copyright (C) 2007, 2013 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 Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 function TreeOutline(listNode)
30 {
31     WebInspector.Object.call(this);
32
33     this.element = listNode;
34
35     this.children = [];
36     this.selectedTreeElement = null;
37     this._childrenListNode = listNode;
38     this._childrenListNode.removeChildren();
39     this._knownTreeElements = [];
40     this._treeElementsExpandedState = [];
41     this.expandTreeElementsWhenArrowing = false;
42     this.allowsRepeatSelection = false;
43     this.root = true;
44     this.hasChildren = false;
45     this.expanded = true;
46     this.selected = false;
47     this.treeOutline = this;
48
49     this._childrenListNode.tabIndex = 0;
50     this._childrenListNode.addEventListener("keydown", this._treeKeyDown.bind(this), true);
51 }
52
53 TreeOutline._knownTreeElementNextIdentifier = 1;
54 TreeOutline.prototype.constructor = TreeOutline;
55
56 TreeOutline.prototype.appendChild = function(child)
57 {
58     if (!child)
59         throw("child can't be undefined or null");
60
61     var lastChild = this.children[this.children.length - 1];
62     if (lastChild) {
63         lastChild.nextSibling = child;
64         child.previousSibling = lastChild;
65     } else {
66         child.previousSibling = null;
67         child.nextSibling = null;
68     }
69
70     var isFirstChild = !this.children.length;
71
72     this.children.push(child);
73     this.hasChildren = true;
74     child.parent = this;
75     child.treeOutline = this.treeOutline;
76     child.treeOutline._rememberTreeElement(child);
77
78     var current = child.children[0];
79     while (current) {
80         current.treeOutline = this.treeOutline;
81         current.treeOutline._rememberTreeElement(current);
82         current = current.traverseNextTreeElement(false, child, true);
83     }
84
85     if (child.hasChildren && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined)
86         child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier];
87
88     if (!this._childrenListNode) {
89         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
90         this._childrenListNode.parentTreeElement = this;
91         this._childrenListNode.classList.add("children");
92         if (this.hidden)
93             this._childrenListNode.classList.add("hidden");
94     }
95
96     child._attach();
97
98     if (this.treeOutline.onadd)
99         this.treeOutline.onadd(child);
100
101     if (isFirstChild && this.expanded)
102         this.expand();
103 }
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     var isFirstChild = !this.children.length;
127
128     this.children.splice(index, 0, child);
129     this.hasChildren = true;
130     child.parent = this;
131     child.treeOutline = this.treeOutline;
132     child.treeOutline._rememberTreeElement(child);
133
134     var current = child.children[0];
135     while (current) {
136         current.treeOutline = this.treeOutline;
137         current.treeOutline._rememberTreeElement(current);
138         current = current.traverseNextTreeElement(false, child, true);
139     }
140
141     if (child.hasChildren && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined)
142         child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier];
143
144     if (!this._childrenListNode) {
145         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
146         this._childrenListNode.parentTreeElement = this;
147         this._childrenListNode.classList.add("children");
148         if (this.hidden)
149             this._childrenListNode.classList.add("hidden");
150     }
151
152     child._attach();
153
154     if (this.treeOutline.onadd)
155         this.treeOutline.onadd(child);
156
157     if (isFirstChild && this.expanded)
158         this.expand();
159 }
160
161 TreeOutline.prototype.removeChildAtIndex = function(childIndex, suppressOnDeselect, suppressSelectSibling)
162 {
163     if (childIndex < 0 || childIndex >= this.children.length)
164         throw("childIndex out of range");
165
166     var child = this.children[childIndex];
167     this.children.splice(childIndex, 1);
168
169     var parent = child.parent;
170     if (child.deselect(suppressOnDeselect)) {
171         if (child.previousSibling && !suppressSelectSibling)
172             child.previousSibling.select(true, false);
173         else if (child.nextSibling && !suppressSelectSibling)
174             child.nextSibling.select(true, false);
175         else if (!suppressSelectSibling)
176             parent.select(true, false);
177     }
178
179     if (child.previousSibling)
180         child.previousSibling.nextSibling = child.nextSibling;
181     if (child.nextSibling)
182         child.nextSibling.previousSibling = child.previousSibling;
183
184     if (child.treeOutline) {
185         child.treeOutline._forgetTreeElement(child);
186         child.treeOutline._forgetChildrenRecursive(child);
187     }
188
189     child._detach();
190     child.treeOutline = null;
191     child.parent = null;
192     child.nextSibling = null;
193     child.previousSibling = null;
194
195     if (this.treeOutline && this.treeOutline.onremove)
196         this.treeOutline.onremove(child);
197 }
198
199 TreeOutline.prototype.removeChild = function(child, suppressOnDeselect, suppressSelectSibling)
200 {
201     if (!child)
202         throw("child can't be undefined or null");
203
204     var childIndex = this.children.indexOf(child);
205     if (childIndex === -1)
206         throw("child not found in this node's children");
207
208     this.removeChildAtIndex(childIndex, suppressOnDeselect, suppressSelectSibling);
209 }
210
211 TreeOutline.prototype.removeChildren = function(suppressOnDeselect)
212 {
213     var treeOutline = this.treeOutline;
214
215     for (var i = 0; i < this.children.length; ++i) {
216         var child = this.children[i];
217         child.deselect(suppressOnDeselect);
218
219         if (child.treeOutline) {
220             child.treeOutline._forgetTreeElement(child);
221             child.treeOutline._forgetChildrenRecursive(child);
222         }
223
224         child._detach();
225         child.treeOutline = null;
226         child.parent = null;
227         child.nextSibling = null;
228         child.previousSibling = null;
229
230         if (treeOutline && treeOutline.onremove)
231             treeOutline.onremove(child);
232     }
233
234     this.children = [];
235 }
236
237 TreeOutline.prototype.removeChildrenRecursive = function(suppressOnDeselect)
238 {
239     var childrenToRemove = this.children;
240
241     var treeOutline = this.treeOutline;
242
243     var child = this.children[0];
244     while (child) {
245         if (child.children.length)
246             childrenToRemove = childrenToRemove.concat(child.children);
247         child = child.traverseNextTreeElement(false, this, true);
248     }
249
250     for (var i = 0; i < childrenToRemove.length; ++i) {
251         child = childrenToRemove[i];
252         child.deselect(suppressOnDeselect);
253
254         if (child.treeOutline)
255             child.treeOutline._forgetTreeElement(child);
256
257         child._detach();
258         child.children = [];
259         child.treeOutline = null;
260         child.parent = null;
261         child.nextSibling = null;
262         child.previousSibling = null;
263
264         if (treeOutline && treeOutline.onremove)
265             treeOutline.onremove(child);
266     }
267
268     this.children = [];
269 }
270
271 TreeOutline.prototype._rememberTreeElement = function(element)
272 {
273     if (!this._knownTreeElements[element.identifier])
274         this._knownTreeElements[element.identifier] = [];
275
276     // check if the element is already known
277     var elements = this._knownTreeElements[element.identifier];
278     if (elements.indexOf(element) !== -1)
279         return;
280
281     // add the element
282     elements.push(element);
283 }
284
285 TreeOutline.prototype._forgetTreeElement = function(element)
286 {
287     if (this.selectedTreeElement === element)
288         this.selectedTreeElement = null;
289     if (this._knownTreeElements[element.identifier])
290         this._knownTreeElements[element.identifier].remove(element, true);
291 }
292
293 TreeOutline.prototype._forgetChildrenRecursive = function(parentElement)
294 {
295     var child = parentElement.children[0];
296     while (child) {
297         this._forgetTreeElement(child);
298         child = child.traverseNextTreeElement(false, parentElement, true);
299     }
300 }
301
302 TreeOutline.prototype.getCachedTreeElement = function(representedObject)
303 {
304     if (!representedObject)
305         return null;
306
307     if (representedObject.__treeElementIdentifier) {
308         // If this representedObject has a tree element identifier, and it is a known TreeElement
309         // in our tree we can just return that tree element.
310         var elements = this._knownTreeElements[representedObject.__treeElementIdentifier];
311         if (elements) {
312             for (var i = 0; i < elements.length; ++i)
313                 if (elements[i].representedObject === representedObject)
314                     return elements[i];
315         }
316     }
317     return null;
318 }
319
320 TreeOutline.prototype.findTreeElement = function(representedObject, isAncestor, getParent)
321 {
322     if (!representedObject)
323         return null;
324
325     var cachedElement = this.getCachedTreeElement(representedObject);
326     if (cachedElement)
327         return cachedElement;
328
329     // The representedObject isn't known, so we start at the top of the tree and work down to find the first
330     // tree element that represents representedObject or one of its ancestors.
331     var item;
332     var found = false;
333     for (var i = 0; i < this.children.length; ++i) {
334         item = this.children[i];
335         if (item.representedObject === representedObject || (isAncestor && isAncestor(item.representedObject, representedObject))) {
336             found = true;
337             break;
338         }
339     }
340
341     if (!found)
342         return null;
343
344     // Make sure the item that we found is connected to the root of the tree.
345     // Build up a list of representedObject's ancestors that aren't already in our tree.
346     var ancestors = [];
347     var currentObject = representedObject;
348     while (currentObject) {
349         ancestors.unshift(currentObject);
350         if (currentObject === item.representedObject)
351             break;
352         currentObject = getParent(currentObject);
353     }
354
355     // For each of those ancestors we populate them to fill in the tree.
356     for (var i = 0; i < ancestors.length; ++i) {
357         // Make sure we don't call findTreeElement with the same representedObject
358         // again, to prevent infinite recursion.
359         if (ancestors[i] === representedObject)
360             continue;
361         // FIXME: we could do something faster than findTreeElement since we will know the next
362         // ancestor exists in the tree.
363         item = this.findTreeElement(ancestors[i], isAncestor, getParent);
364         if (item)
365             item.onpopulate();
366     }
367
368     return this.getCachedTreeElement(representedObject);
369 }
370
371 TreeOutline.prototype._treeElementDidChange = function(treeElement)
372 {
373     if (treeElement.treeOutline !== this)
374         return;
375
376     if (this.onchange)
377         this.onchange(treeElement);
378 }
379
380 TreeOutline.prototype.treeElementFromPoint = function(x, y)
381 {
382     var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y);
383     if (!node)
384         return null;
385
386     var listNode = node.enclosingNodeOrSelfWithNodeNameInArray(["ol", "li"]);
387     if (listNode)
388         return listNode.parentTreeElement || listNode.treeElement;
389     return null;
390 }
391
392 TreeOutline.prototype._treeKeyDown = function(event)
393 {
394     if (event.target !== this._childrenListNode)
395         return;
396
397     if (!this.selectedTreeElement || event.shiftKey || event.metaKey || event.ctrlKey)
398         return;
399
400     var handled = false;
401     var nextSelectedElement;
402     if (event.keyIdentifier === "Up" && !event.altKey) {
403         nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true);
404         while (nextSelectedElement && !nextSelectedElement.selectable)
405             nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(!this.expandTreeElementsWhenArrowing);
406         handled = nextSelectedElement ? true : false;
407     } else if (event.keyIdentifier === "Down" && !event.altKey) {
408         nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true);
409         while (nextSelectedElement && !nextSelectedElement.selectable)
410             nextSelectedElement = nextSelectedElement.traverseNextTreeElement(!this.expandTreeElementsWhenArrowing);
411         handled = nextSelectedElement ? true : false;
412     } else if (event.keyIdentifier === "Left") {
413         if (this.selectedTreeElement.expanded) {
414             if (event.altKey)
415                 this.selectedTreeElement.collapseRecursively();
416             else
417                 this.selectedTreeElement.collapse();
418             handled = true;
419         } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) {
420             handled = true;
421             if (this.selectedTreeElement.parent.selectable) {
422                 nextSelectedElement = this.selectedTreeElement.parent;
423                 while (nextSelectedElement && !nextSelectedElement.selectable)
424                     nextSelectedElement = nextSelectedElement.parent;
425                 handled = nextSelectedElement ? true : false;
426             } else if (this.selectedTreeElement.parent)
427                 this.selectedTreeElement.parent.collapse();
428         }
429     } else if (event.keyIdentifier === "Right") {
430         if (!this.selectedTreeElement.revealed()) {
431             this.selectedTreeElement.reveal();
432             handled = true;
433         } else if (this.selectedTreeElement.hasChildren) {
434             handled = true;
435             if (this.selectedTreeElement.expanded) {
436                 nextSelectedElement = this.selectedTreeElement.children[0];
437                 while (nextSelectedElement && !nextSelectedElement.selectable)
438                     nextSelectedElement = nextSelectedElement.nextSibling;
439                 handled = nextSelectedElement ? true : false;
440             } else {
441                 if (event.altKey)
442                     this.selectedTreeElement.expandRecursively();
443                 else
444                     this.selectedTreeElement.expand();
445             }
446         }
447     } else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */) {
448         if (this.selectedTreeElement.ondelete)
449             handled = this.selectedTreeElement.ondelete();
450         if (!handled && this.treeOutline.ondelete)
451             handled = this.treeOutline.ondelete(this.selectedTreeElement);
452     } else if (isEnterKey(event)) {
453         if (this.selectedTreeElement.onenter)
454             handled = this.selectedTreeElement.onenter();
455         if (!handled && this.treeOutline.onenter)
456             handled = this.treeOutline.onenter(this.selectedTreeElement);
457     } else if (event.keyIdentifier === "U+0020" /* Space */) {
458         if (this.selectedTreeElement.onspace)
459             handled = this.selectedTreeElement.onspace();
460         if (!handled && this.treeOutline.onspace)
461             handled = this.treeOutline.onspace(this.selectedTreeElement);
462     }
463
464     if (nextSelectedElement) {
465         nextSelectedElement.reveal();
466         nextSelectedElement.select(false, true);
467     }
468
469     if (handled) {
470         event.preventDefault();
471         event.stopPropagation();
472     }
473 }
474
475 TreeOutline.prototype.expand = function()
476 {
477     // this is the root, do nothing
478 }
479
480 TreeOutline.prototype.collapse = function()
481 {
482     // this is the root, do nothing
483 }
484
485 TreeOutline.prototype.revealed = function()
486 {
487     return true;
488 }
489
490 TreeOutline.prototype.reveal = function()
491 {
492     // this is the root, do nothing
493 }
494
495 TreeOutline.prototype.select = function()
496 {
497     // this is the root, do nothing
498 }
499
500 TreeOutline.prototype.revealAndSelect = function(omitFocus)
501 {
502     // this is the root, do nothing
503 }
504
505 TreeOutline.prototype.__proto__ = WebInspector.Object.prototype;
506
507 function TreeElement(title, representedObject, hasChildren)
508 {
509     WebInspector.Object.call(this);
510
511     this._title = title;
512     this.representedObject = (representedObject || {});
513
514     if (this.representedObject.__treeElementIdentifier)
515         this.identifier = this.representedObject.__treeElementIdentifier;
516     else {
517         this.identifier = TreeOutline._knownTreeElementNextIdentifier++;
518         this.representedObject.__treeElementIdentifier = this.identifier;
519     }
520
521     this._hidden = false;
522     this._selectable = true;
523     this.expanded = false;
524     this.selected = false;
525     this.hasChildren = hasChildren;
526     this.children = [];
527     this.treeOutline = null;
528     this.parent = null;
529     this.previousSibling = null;
530     this.nextSibling = null;
531     this._listItemNode = null;
532 }
533
534 TreeElement.prototype = {
535     constructor: TreeElement,
536
537     arrowToggleWidth: 10,
538
539     get selectable() {
540         if (this._hidden)
541             return false;
542         return this._selectable;
543     },
544
545     set selectable(x) {
546         this._selectable = x;
547     },
548
549     get listItemElement() {
550         return this._listItemNode;
551     },
552
553     get childrenListElement() {
554         return this._childrenListNode;
555     },
556
557     get title() {
558         return this._title;
559     },
560
561     set title(x) {
562         this._title = x;
563         this._setListItemNodeContent();
564         this.didChange();
565     },
566
567     get titleHTML() {
568         return this._titleHTML;
569     },
570
571     set titleHTML(x) {
572         this._titleHTML = x;
573         this._setListItemNodeContent();
574         this.didChange();
575     },
576
577     get tooltip() {
578         return this._tooltip;
579     },
580
581     set tooltip(x) {
582         this._tooltip = x;
583         if (this._listItemNode)
584             this._listItemNode.title = x ? x : "";
585         this.didChange();
586     },
587
588     get hasChildren() {
589         return this._hasChildren;
590     },
591
592     set hasChildren(x) {
593         if (this._hasChildren === x)
594             return;
595
596         this._hasChildren = x;
597
598         if (!this._listItemNode)
599             return;
600
601         if (x)
602             this._listItemNode.classList.add("parent");
603         else {
604             this._listItemNode.classList.remove("parent");
605             this.collapse();
606         }
607
608         this.didChange();
609     },
610
611     get hidden() {
612         return this._hidden;
613     },
614
615     set hidden(x) {
616         if (this._hidden === x)
617             return;
618
619         this._hidden = x;
620
621         if (x) {
622             if (this._listItemNode)
623                 this._listItemNode.classList.add("hidden");
624             if (this._childrenListNode)
625                 this._childrenListNode.classList.add("hidden");
626         } else {
627             if (this._listItemNode)
628                 this._listItemNode.classList.remove("hidden");
629             if (this._childrenListNode)
630                 this._childrenListNode.classList.remove("hidden");
631         }
632
633         if (this.treeOutline && this.treeOutline.onhidden)
634             this.treeOutline.onhidden(this, x);
635     },
636
637     get shouldRefreshChildren() {
638         return this._shouldRefreshChildren;
639     },
640
641     set shouldRefreshChildren(x) {
642         this._shouldRefreshChildren = x;
643         if (x && this.expanded)
644             this.expand();
645     },
646
647     _fireDidChange: function()
648     {
649         delete this._didChangeTimeoutIdentifier;
650
651         if (this.treeOutline)
652             this.treeOutline._treeElementDidChange(this);
653     },
654
655     didChange: function()
656     {
657         if (!this.treeOutline)
658             return;
659
660         // Prevent telling the TreeOutline multiple times in a row by delaying it with a timeout.
661         if (!this._didChangeTimeoutIdentifier)
662             this._didChangeTimeoutIdentifier = setTimeout(this._fireDidChange.bind(this), 0);
663     },
664
665     _setListItemNodeContent: function()
666     {
667         if (!this._listItemNode)
668             return;
669
670         if (!this._titleHTML && !this._title)
671             this._listItemNode.removeChildren();
672         else if (typeof this._titleHTML === "string")
673             this._listItemNode.innerHTML = this._titleHTML;
674         else if (typeof this._title === "string")
675             this._listItemNode.textContent = this._title;
676         else {
677             this._listItemNode.removeChildren();
678             if (this._title.parentNode)
679                 this._title.parentNode.removeChild(this._title);
680             this._listItemNode.appendChild(this._title);
681         }
682     }
683 }
684
685 TreeElement.prototype.appendChild = TreeOutline.prototype.appendChild;
686 TreeElement.prototype.insertChild = TreeOutline.prototype.insertChild;
687 TreeElement.prototype.removeChild = TreeOutline.prototype.removeChild;
688 TreeElement.prototype.removeChildAtIndex = TreeOutline.prototype.removeChildAtIndex;
689 TreeElement.prototype.removeChildren = TreeOutline.prototype.removeChildren;
690 TreeElement.prototype.removeChildrenRecursive = TreeOutline.prototype.removeChildrenRecursive;
691
692 TreeElement.prototype._attach = function()
693 {
694     if (!this._listItemNode || this.parent._shouldRefreshChildren) {
695         if (this._listItemNode && this._listItemNode.parentNode)
696             this._listItemNode.parentNode.removeChild(this._listItemNode);
697
698         this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
699         this._listItemNode.treeElement = this;
700         this._setListItemNodeContent();
701         this._listItemNode.title = this._tooltip ? this._tooltip : "";
702
703         if (this.hidden)
704             this._listItemNode.classList.add("hidden");
705         if (this.hasChildren)
706             this._listItemNode.classList.add("parent");
707         if (this.expanded)
708             this._listItemNode.classList.add("expanded");
709         if (this.selected)
710             this._listItemNode.classList.add("selected");
711
712         this._listItemNode.addEventListener("mousedown", TreeElement.treeElementMouseDown, false);
713         this._listItemNode.addEventListener("click", TreeElement.treeElementToggled, false);
714         this._listItemNode.addEventListener("dblclick", TreeElement.treeElementDoubleClicked, false);
715
716         if (this.onattach)
717             this.onattach(this);
718     }
719
720     var nextSibling = null;
721     if (this.nextSibling && this.nextSibling._listItemNode && this.nextSibling._listItemNode.parentNode === this.parent._childrenListNode)
722         nextSibling = this.nextSibling._listItemNode;
723     this.parent._childrenListNode.insertBefore(this._listItemNode, nextSibling);
724     if (this._childrenListNode)
725         this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
726     if (this.selected)
727         this.select();
728     if (this.expanded)
729         this.expand();
730 }
731
732 TreeElement.prototype._detach = function()
733 {
734     if (this.ondetach)
735         this.ondetach(this);
736     if (this._listItemNode && this._listItemNode.parentNode)
737         this._listItemNode.parentNode.removeChild(this._listItemNode);
738     if (this._childrenListNode && this._childrenListNode.parentNode)
739         this._childrenListNode.parentNode.removeChild(this._childrenListNode);
740 }
741
742 TreeElement.treeElementMouseDown = function(event)
743 {
744     var element = event.currentTarget;
745     if (!element || !element.treeElement || !element.treeElement.selectable)
746         return;
747
748     if (element.treeElement.isEventWithinDisclosureTriangle(event)) {
749         event.preventDefault();
750         return;
751     }
752
753     element.treeElement.selectOnMouseDown(event);
754 }
755
756 TreeElement.treeElementToggled = function(event)
757 {
758     var element = event.currentTarget;
759     if (!element || !element.treeElement)
760         return;
761
762     var toggleOnClick = element.treeElement.toggleOnClick && !element.treeElement.selectable;
763     var isInTriangle = element.treeElement.isEventWithinDisclosureTriangle(event);
764     if (!toggleOnClick && !isInTriangle)
765         return;
766
767     if (element.treeElement.expanded) {
768         if (event.altKey)
769             element.treeElement.collapseRecursively();
770         else
771             element.treeElement.collapse();
772     } else {
773         if (event.altKey)
774             element.treeElement.expandRecursively();
775         else
776             element.treeElement.expand();
777     }
778     event.stopPropagation();
779 }
780
781 TreeElement.treeElementDoubleClicked = function(event)
782 {
783     var element = event.currentTarget;
784     if (!element || !element.treeElement)
785         return;
786
787     if (element.treeElement.isEventWithinDisclosureTriangle(event))
788         return;
789
790     if (element.treeElement.ondblclick)
791         element.treeElement.ondblclick.call(element.treeElement, event);
792     else if (element.treeElement.hasChildren && !element.treeElement.expanded)
793         element.treeElement.expand();
794 }
795
796 TreeElement.prototype.collapse = function()
797 {
798     if (this._listItemNode)
799         this._listItemNode.classList.remove("expanded");
800     if (this._childrenListNode)
801         this._childrenListNode.classList.remove("expanded");
802
803     this.expanded = false;
804     if (this.treeOutline)
805         this.treeOutline._treeElementsExpandedState[this.identifier] = false;
806
807     if (this.oncollapse)
808         this.oncollapse(this);
809
810     if (this.treeOutline && this.treeOutline.oncollapse)
811         this.treeOutline.oncollapse(this);
812 }
813
814 TreeElement.prototype.collapseRecursively = function()
815 {
816     var item = this;
817     while (item) {
818         if (item.expanded)
819             item.collapse();
820         item = item.traverseNextTreeElement(false, this, true);
821     }
822 }
823
824 TreeElement.prototype.expand = function()
825 {
826     if (this.expanded && !this._shouldRefreshChildren && this._childrenListNode)
827         return;
828
829     // Set this before onpopulate. Since onpopulate can add elements and call onadd, this makes
830     // sure the expanded flag is true before calling those functions. This prevents the possibility
831     // of an infinite loop if onpopulate or onadd were to call expand.
832
833     this.expanded = true;
834     if (this.treeOutline)
835         this.treeOutline._treeElementsExpandedState[this.identifier] = true;
836
837     // If there are no children, return. We will be expanded once we have children.
838     if (!this.hasChildren)
839         return;
840
841     if (this.treeOutline && (!this._childrenListNode || this._shouldRefreshChildren)) {
842         if (this._childrenListNode && this._childrenListNode.parentNode)
843             this._childrenListNode.parentNode.removeChild(this._childrenListNode);
844
845         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
846         this._childrenListNode.parentTreeElement = this;
847         this._childrenListNode.classList.add("children");
848
849         if (this.hidden)
850             this._childrenListNode.classList.add("hidden");
851
852         this.onpopulate();
853
854         for (var i = 0; i < this.children.length; ++i)
855             this.children[i]._attach();
856
857         delete this._shouldRefreshChildren;
858     }
859
860     if (this._listItemNode) {
861         this._listItemNode.classList.add("expanded");
862         if (this._childrenListNode && this._childrenListNode.parentNode != this._listItemNode.parentNode)
863             this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
864     }
865
866     if (this._childrenListNode)
867         this._childrenListNode.classList.add("expanded");
868
869     if (this.onexpand)
870         this.onexpand(this);
871
872     if (this.treeOutline && this.treeOutline.onexpand)
873         this.treeOutline.onexpand(this);
874 }
875
876 TreeElement.prototype.expandRecursively = function(maxDepth)
877 {
878     var item = this;
879     var info = {};
880     var depth = 0;
881
882     // The Inspector uses TreeOutlines to represents object properties, so recursive expansion
883     // in some case can be infinite, since JavaScript objects can hold circular references.
884     // So default to a recursion cap of 3 levels, since that gives fairly good results.
885     if (typeof maxDepth === "undefined" || typeof maxDepth === "null")
886         maxDepth = 3;
887
888     while (item) {
889         if (depth < maxDepth)
890             item.expand();
891         item = item.traverseNextTreeElement(false, this, (depth >= maxDepth), info);
892         depth += info.depthChange;
893     }
894 }
895
896 TreeElement.prototype.hasAncestor = function(ancestor) {
897     if (!ancestor)
898         return false;
899
900     var currentNode = this.parent;
901     while (currentNode) {
902         if (ancestor === currentNode)
903             return true;
904         currentNode = currentNode.parent;
905     }
906
907     return false;
908 }
909
910 TreeElement.prototype.reveal = function()
911 {
912     var currentAncestor = this.parent;
913     while (currentAncestor && !currentAncestor.root) {
914         if (!currentAncestor.expanded)
915             currentAncestor.expand();
916         currentAncestor = currentAncestor.parent;
917     }
918
919     if (this.onreveal)
920         this.onreveal(this);
921 }
922
923 TreeElement.prototype.revealed = function()
924 {
925     var currentAncestor = this.parent;
926     while (currentAncestor && !currentAncestor.root) {
927         if (!currentAncestor.expanded)
928             return false;
929         currentAncestor = currentAncestor.parent;
930     }
931
932     return true;
933 }
934
935 TreeElement.prototype.selectOnMouseDown = function(event)
936 {
937     this.select(false, true);
938 }
939
940 TreeElement.prototype.select = function(omitFocus, selectedByUser, suppressOnSelect, suppressOnDeselect)
941 {
942     if (!this.treeOutline || !this.selectable)
943         return;
944
945     if (this.selected && !this.treeOutline.allowsRepeatSelection)
946         return;
947
948     if (!omitFocus)
949         this.treeOutline._childrenListNode.focus();
950
951     // Focusing on another node may detach "this" from tree.
952     if (!this.treeOutline)
953         return;
954
955     this.treeOutline.processingSelectionChange = true;
956
957     if (!this.selected) {
958         if (this.treeOutline.selectedTreeElement)
959             this.treeOutline.selectedTreeElement.deselect(suppressOnDeselect);
960
961         this.selected = true;
962         this.treeOutline.selectedTreeElement = this;
963
964         if (this._listItemNode)
965             this._listItemNode.classList.add("selected");
966     }
967
968     if (this.onselect && !suppressOnSelect)
969         this.onselect(this, selectedByUser);
970
971     if (this.treeOutline.onselect && !suppressOnSelect)
972         this.treeOutline.onselect(this, selectedByUser);
973
974     delete this.treeOutline.processingSelectionChange;
975 }
976
977 TreeElement.prototype.revealAndSelect = function(omitFocus, selectedByUser, suppressOnSelect, suppressOnDeselect)
978 {
979     this.reveal();
980     this.select(omitFocus, selectedByUser, suppressOnSelect, suppressOnDeselect);
981 }
982
983 TreeElement.prototype.deselect = function(suppressOnDeselect)
984 {
985     if (!this.treeOutline || this.treeOutline.selectedTreeElement !== this || !this.selected)
986         return false;
987
988     this.selected = false;
989     this.treeOutline.selectedTreeElement = null;
990
991     if (this._listItemNode)
992         this._listItemNode.classList.remove("selected");
993
994     if (this.ondeselect && !suppressOnDeselect)
995         this.ondeselect(this);
996
997     if (this.treeOutline.ondeselect && !suppressOnDeselect)
998         this.treeOutline.ondeselect(this);
999
1000     return true;
1001 }
1002
1003 TreeElement.prototype.onpopulate = function()
1004 {
1005     // Overriden by subclasses.
1006 }
1007
1008 TreeElement.prototype.traverseNextTreeElement = function(skipUnrevealed, stayWithin, dontPopulate, info)
1009 {
1010     if (!dontPopulate && this.hasChildren)
1011         this.onpopulate.call(this); // FIXME: This shouldn't need to use call, but this is working around a JSC bug. https://webkit.org/b/74811
1012
1013     if (info)
1014         info.depthChange = 0;
1015
1016     var element = skipUnrevealed ? (this.revealed() ? this.children[0] : null) : this.children[0];
1017     if (element && (!skipUnrevealed || (skipUnrevealed && this.expanded))) {
1018         if (info)
1019             info.depthChange = 1;
1020         return element;
1021     }
1022
1023     if (this === stayWithin)
1024         return null;
1025
1026     element = skipUnrevealed ? (this.revealed() ? this.nextSibling : null) : this.nextSibling;
1027     if (element)
1028         return element;
1029
1030     element = this;
1031     while (element && !element.root && !(skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) && element.parent !== stayWithin) {
1032         if (info)
1033             info.depthChange -= 1;
1034         element = element.parent;
1035     }
1036
1037     if (!element)
1038         return null;
1039
1040     return (skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling);
1041 }
1042
1043 TreeElement.prototype.traversePreviousTreeElement = function(skipUnrevealed, dontPopulate)
1044 {
1045     var element = skipUnrevealed ? (this.revealed() ? this.previousSibling : null) : this.previousSibling;
1046     if (!dontPopulate && element && element.hasChildren)
1047         element.onpopulate();
1048
1049     while (element && (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1])) {
1050         if (!dontPopulate && element.hasChildren)
1051             element.onpopulate();
1052         element = (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1]);
1053     }
1054
1055     if (element)
1056         return element;
1057
1058     if (!this.parent || this.parent.root)
1059         return null;
1060
1061     return this.parent;
1062 }
1063
1064 TreeElement.prototype.isEventWithinDisclosureTriangle = function(event)
1065 {
1066     // FIXME: We should not use getComputedStyle(). For that we need to get rid of using ::before for disclosure triangle. (http://webk.it/74446)
1067     var computedLeftPadding = window.getComputedStyle(this._listItemNode).getPropertyCSSValue("padding-left").getFloatValue(CSSPrimitiveValue.CSS_PX);
1068     var left = this._listItemNode.totalOffsetLeft + computedLeftPadding;
1069     return event.pageX >= left && event.pageX <= left + this.arrowToggleWidth && this.hasChildren;
1070 }
1071
1072 TreeElement.prototype.__proto__ = WebInspector.Object.prototype;