Web Inspector: more robust treeoutline.findTreeElement
[WebKit.git] / Source / WebCore / inspector / front-end / 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 {boolean=} nonFocusable
32  */
33 function TreeOutline(listNode, nonFocusable)
34 {
35     /**
36      * @type {Array.<TreeElement>}
37      */
38     this.children = [];
39     this.selectedTreeElement = null;
40     this._childrenListNode = listNode;
41     this.childrenListElement = this._childrenListNode;
42     this._childrenListNode.removeChildren();
43     this.expandTreeElementsWhenArrowing = false;
44     this.root = true;
45     this.hasChildren = false;
46     this.expanded = true;
47     this.selected = false;
48     this.treeOutline = this;
49     this.comparator = null;
50     this.searchable = false;
51     this.searchInputElement = null;
52
53     this.setFocusable(!nonFocusable);
54     this._childrenListNode.addEventListener("keydown", this._treeKeyDown.bind(this), true);
55     this._childrenListNode.addEventListener("keypress", this._treeKeyPress.bind(this), true);
56     
57     this._treeElementsMap = new Map();
58     this._expandedStateMap = new Map();
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 TreeOutline.prototype.appendChild = function(child)
70 {
71     var insertionIndex;
72     if (this.treeOutline.comparator)
73         insertionIndex = insertionIndexForObjectInListSortedByFunction(child, this.children, this.treeOutline.comparator);
74     else
75         insertionIndex = this.children.length;
76     this.insertChild(child, insertionIndex);
77 }
78
79 TreeOutline.prototype.insertChild = function(child, index)
80 {
81     if (!child)
82         throw("child can't be undefined or null");
83
84     var previousChild = (index > 0 ? this.children[index - 1] : null);
85     if (previousChild) {
86         previousChild.nextSibling = child;
87         child.previousSibling = previousChild;
88     } else {
89         child.previousSibling = null;
90     }
91
92     var nextChild = this.children[index];
93     if (nextChild) {
94         nextChild.previousSibling = child;
95         child.nextSibling = nextChild;
96     } else {
97         child.nextSibling = null;
98     }
99
100     this.children.splice(index, 0, child);
101     this.hasChildren = true;
102     child.parent = this;
103     child.treeOutline = this.treeOutline;
104     child.treeOutline._rememberTreeElement(child);
105
106     var current = child.children[0];
107     while (current) {
108         current.treeOutline = this.treeOutline;
109         current.treeOutline._rememberTreeElement(current);
110         current = current.traverseNextTreeElement(false, child, true);
111     }
112
113     if (child.hasChildren && typeof(child.treeOutline._expandedStateMap.get(child.representedObject)) !== "undefined")
114         child.expanded = child.treeOutline._expandedStateMap.get(child.representedObject);
115
116     if (!this._childrenListNode) {
117         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
118         this._childrenListNode.parentTreeElement = this;
119         this._childrenListNode.classList.add("children");
120         if (this.hidden)
121             this._childrenListNode.classList.add("hidden");
122     }
123
124     child._attach();
125 }
126
127 TreeOutline.prototype.removeChildAtIndex = function(childIndex)
128 {
129     if (childIndex < 0 || childIndex >= this.children.length)
130         throw("childIndex out of range");
131
132     var child = this.children[childIndex];
133     this.children.splice(childIndex, 1);
134
135     var parent = child.parent;
136     if (child.deselect()) {
137         if (child.previousSibling)
138             child.previousSibling.select();
139         else if (child.nextSibling)
140             child.nextSibling.select();
141         else
142             parent.select();
143     }
144
145     if (child.previousSibling)
146         child.previousSibling.nextSibling = child.nextSibling;
147     if (child.nextSibling)
148         child.nextSibling.previousSibling = child.previousSibling;
149
150     if (child.treeOutline) {
151         child.treeOutline._forgetTreeElement(child);
152         child.treeOutline._forgetChildrenRecursive(child);
153     }
154
155     child._detach();
156     child.treeOutline = null;
157     child.parent = null;
158     child.nextSibling = null;
159     child.previousSibling = null;
160 }
161
162 TreeOutline.prototype.removeChild = function(child)
163 {
164     if (!child)
165         throw("child can't be undefined or null");
166
167     var childIndex = this.children.indexOf(child);
168     if (childIndex === -1)
169         throw("child not found in this node's children");
170
171     this.removeChildAtIndex.call(this, childIndex);
172 }
173
174 TreeOutline.prototype.removeChildren = function()
175 {
176     for (var i = 0; i < this.children.length; ++i) {
177         var child = this.children[i];
178         child.deselect();
179
180         if (child.treeOutline) {
181             child.treeOutline._forgetTreeElement(child);
182             child.treeOutline._forgetChildrenRecursive(child);
183         }
184
185         child._detach();
186         child.treeOutline = null;
187         child.parent = null;
188         child.nextSibling = null;
189         child.previousSibling = null;
190     }
191
192     this.children = [];
193 }
194
195 TreeOutline.prototype._rememberTreeElement = function(element)
196 {
197     if (!this._treeElementsMap.get(element.representedObject))
198         this._treeElementsMap.put(element.representedObject, []);
199         
200     // check if the element is already known
201     var elements = this._treeElementsMap.get(element.representedObject);
202     if (elements.indexOf(element) !== -1)
203         return;
204
205     // add the element
206     elements.push(element);
207 }
208
209 TreeOutline.prototype._forgetTreeElement = function(element)
210 {
211     if (this._treeElementsMap.get(element.representedObject)) {
212         var elements = this._treeElementsMap.get(element.representedObject);
213         elements.remove(element, true);
214         if (!elements.length)
215             this._treeElementsMap.remove(element.representedObject);
216     }
217 }
218
219 TreeOutline.prototype._forgetChildrenRecursive = function(parentElement)
220 {
221     var child = parentElement.children[0];
222     while (child) {
223         this._forgetTreeElement(child);
224         child = child.traverseNextTreeElement(false, parentElement, true);
225     }
226 }
227
228 TreeOutline.prototype.getCachedTreeElement = function(representedObject)
229 {
230     if (!representedObject)
231         return null;
232
233     var elements = this._treeElementsMap.get(representedObject);
234     if (elements && elements.length)
235         return elements[0];
236     return null;
237 }
238
239 TreeOutline.prototype.findTreeElement = function(representedObject, isAncestor, getParent)
240 {
241     if (!representedObject)
242         return null;
243
244     var cachedElement = this.getCachedTreeElement(representedObject);
245     if (cachedElement)
246         return cachedElement;
247
248     // Walk up the parent pointers from the desired representedObject 
249     var ancestors = [];
250     for (var currentObject = getParent(representedObject); currentObject;  currentObject = getParent(currentObject)) {
251         ancestors.push(currentObject);
252         if (this.getCachedTreeElement(currentObject))  // stop climbing as soon as we hit
253             break;
254     }
255         
256     if (!currentObject)
257         return null;
258
259     // Walk down to populate each ancestor's children, to fill in the tree and the cache.
260     for (var i = ancestors.length - 1; i >= 0; --i) {
261         var treeElement = this.getCachedTreeElement(ancestors[i]);
262         if (treeElement)
263             treeElement.onpopulate();  // fill the cache with the children of treeElement
264     }
265
266     return this.getCachedTreeElement(representedObject);
267 }
268
269 TreeOutline.prototype.treeElementFromPoint = function(x, y)
270 {
271     var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y);
272     if (!node)
273         return null;
274
275     var listNode = node.enclosingNodeOrSelfWithNodeNameInArray(["ol", "li"]);
276     if (listNode)
277         return listNode.parentTreeElement || listNode.treeElement;
278     return null;
279 }
280
281 TreeOutline.prototype._treeKeyPress = function(event)
282 {
283     if (!this.searchable || WebInspector.isBeingEdited(this._childrenListNode))
284         return;
285     
286     var searchText = String.fromCharCode(event.charCode);
287     // Ignore whitespace.
288     if (searchText.trim() !== searchText)
289         return;
290
291     this._startSearch(searchText);
292     event.consume(true);
293 }
294
295 TreeOutline.prototype._treeKeyDown = function(event)
296 {
297     if (event.target !== this._childrenListNode)
298         return;
299
300     if (!this.selectedTreeElement || event.shiftKey || event.metaKey || event.ctrlKey)
301         return;
302
303     var handled = false;
304     var nextSelectedElement;
305     if (event.keyIdentifier === "Up" && !event.altKey) {
306         nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true);
307         while (nextSelectedElement && !nextSelectedElement.selectable)
308             nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(!this.expandTreeElementsWhenArrowing);
309         handled = nextSelectedElement ? true : false;
310     } else if (event.keyIdentifier === "Down" && !event.altKey) {
311         nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true);
312         while (nextSelectedElement && !nextSelectedElement.selectable)
313             nextSelectedElement = nextSelectedElement.traverseNextTreeElement(!this.expandTreeElementsWhenArrowing);
314         handled = nextSelectedElement ? true : false;
315     } else if (event.keyIdentifier === "Left") {
316         if (this.selectedTreeElement.expanded) {
317             if (event.altKey)
318                 this.selectedTreeElement.collapseRecursively();
319             else
320                 this.selectedTreeElement.collapse();
321             handled = true;
322         } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) {
323             handled = true;
324             if (this.selectedTreeElement.parent.selectable) {
325                 nextSelectedElement = this.selectedTreeElement.parent;
326                 while (nextSelectedElement && !nextSelectedElement.selectable)
327                     nextSelectedElement = nextSelectedElement.parent;
328                 handled = nextSelectedElement ? true : false;
329             } else if (this.selectedTreeElement.parent)
330                 this.selectedTreeElement.parent.collapse();
331         }
332     } else if (event.keyIdentifier === "Right") {
333         if (!this.selectedTreeElement.revealed()) {
334             this.selectedTreeElement.reveal();
335             handled = true;
336         } else if (this.selectedTreeElement.hasChildren) {
337             handled = true;
338             if (this.selectedTreeElement.expanded) {
339                 nextSelectedElement = this.selectedTreeElement.children[0];
340                 while (nextSelectedElement && !nextSelectedElement.selectable)
341                     nextSelectedElement = nextSelectedElement.nextSibling;
342                 handled = nextSelectedElement ? true : false;
343             } else {
344                 if (event.altKey)
345                     this.selectedTreeElement.expandRecursively();
346                 else
347                     this.selectedTreeElement.expand();
348             }
349         }
350     } else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */)
351         handled = this.selectedTreeElement.ondelete();
352     else if (isEnterKey(event))
353         handled = this.selectedTreeElement.onenter();
354     else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Space.code)
355         handled = this.selectedTreeElement.onspace();
356
357     if (nextSelectedElement) {
358         nextSelectedElement.reveal();
359         nextSelectedElement.select(false, true);
360     }
361
362     if (handled)
363         event.consume(true);
364 }
365
366 TreeOutline.prototype.expand = function()
367 {
368     // this is the root, do nothing
369 }
370
371 TreeOutline.prototype.collapse = function()
372 {
373     // this is the root, do nothing
374 }
375
376 TreeOutline.prototype.revealed = function()
377 {
378     return true;
379 }
380
381 TreeOutline.prototype.reveal = function()
382 {
383     // this is the root, do nothing
384 }
385
386 TreeOutline.prototype.select = function()
387 {
388     // this is the root, do nothing
389 }
390
391 /**
392  * @param {boolean=} omitFocus
393  */
394 TreeOutline.prototype.revealAndSelect = function(omitFocus)
395 {
396     // this is the root, do nothing
397 }
398
399 /**
400  * @param {string} searchText
401  */
402 TreeOutline.prototype._startSearch = function(searchText)
403 {
404     if (!this.searchInputElement || !this.searchable)
405         return;
406     
407     this._searching = true;
408
409     if (this.searchStarted)
410         this.searchStarted();
411     
412     this.searchInputElement.value = searchText;
413     
414     function focusSearchInput()
415     {
416         this.searchInputElement.focus();
417     }
418     window.setTimeout(focusSearchInput.bind(this), 0);
419     this._searchTextChanged();
420     this._boundSearchTextChanged = this._searchTextChanged.bind(this);
421     this.searchInputElement.addEventListener("paste", this._boundSearchTextChanged);
422     this.searchInputElement.addEventListener("cut", this._boundSearchTextChanged);
423     this.searchInputElement.addEventListener("keypress", this._boundSearchTextChanged);
424     this._boundSearchInputKeyDown = this._searchInputKeyDown.bind(this);
425     this.searchInputElement.addEventListener("keydown", this._boundSearchInputKeyDown);
426     this._boundSearchInputBlur = this._searchInputBlur.bind(this);
427     this.searchInputElement.addEventListener("blur", this._boundSearchInputBlur);
428 }
429
430 TreeOutline.prototype._searchTextChanged = function()
431 {
432     function updateSearch()
433     {
434         var nextSelectedElement = this._nextSearchMatch(this.searchInputElement.value, this.selectedTreeElement, false);
435         if (!nextSelectedElement)
436             nextSelectedElement = this._nextSearchMatch(this.searchInputElement.value, this.children[0], false);
437         this._showSearchMatchElement(nextSelectedElement);
438     }
439     
440     window.setTimeout(updateSearch.bind(this), 0);
441 }
442
443 TreeOutline.prototype._showSearchMatchElement = function(treeElement)
444 {
445     this._currentSearchMatchElement = treeElement;
446     if (treeElement) {
447         this._childrenListNode.classList.add("search-match-found");
448         this._childrenListNode.classList.remove("search-match-not-found");
449         treeElement.revealAndSelect(true);
450     } else {
451         this._childrenListNode.classList.remove("search-match-found");
452         this._childrenListNode.classList.add("search-match-not-found");
453     }
454 }
455
456 TreeOutline.prototype._searchInputKeyDown = function(event)
457 {
458     if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey)
459         return;
460
461     var handled = false;
462     var nextSelectedElement;
463     if (event.keyIdentifier === "Down") {
464         nextSelectedElement = this._nextSearchMatch(this.searchInputElement.value, this.selectedTreeElement, true);
465         handled = true;
466     } else if (event.keyIdentifier === "Up") {
467         nextSelectedElement = this._previousSearchMatch(this.searchInputElement.value, this.selectedTreeElement);
468         handled = true;
469     } else if (event.keyCode === 27 /* Esc */) {
470         this._searchFinished();
471         handled = true;
472     } else if (isEnterKey(event)) {
473         var lastSearchMatchElement = this._currentSearchMatchElement;
474         this._searchFinished();
475         lastSearchMatchElement.onenter();
476         handled = true;
477     }
478
479     if (nextSelectedElement)
480         this._showSearchMatchElement(nextSelectedElement);
481         
482     if (handled)
483         event.consume(true);
484     else
485        window.setTimeout(this._boundSearchTextChanged, 0); 
486 }
487
488 /**
489  * @param {string} searchText
490  * @param {TreeElement} startTreeElement
491  * @param {boolean} skipStartTreeElement
492  */
493 TreeOutline.prototype._nextSearchMatch = function(searchText, startTreeElement, skipStartTreeElement)
494 {
495     var currentTreeElement = startTreeElement;
496     var skipCurrentTreeElement = skipStartTreeElement;
497     while (currentTreeElement && (skipCurrentTreeElement || !currentTreeElement.matchesSearchText || !currentTreeElement.matchesSearchText(searchText))) {
498         currentTreeElement = currentTreeElement.traverseNextTreeElement(true, null, true);
499         skipCurrentTreeElement = false;
500     }
501
502     return currentTreeElement;
503 }
504
505 /**
506  * @param {string} searchText
507  * @param {TreeElement=} startTreeElement
508  */
509 TreeOutline.prototype._previousSearchMatch = function(searchText, startTreeElement)
510 {
511     var currentTreeElement = startTreeElement;
512     var skipCurrentTreeElement = true;
513     while (currentTreeElement && (skipCurrentTreeElement || !currentTreeElement.matchesSearchText || !currentTreeElement.matchesSearchText(searchText))) {
514         currentTreeElement = currentTreeElement.traversePreviousTreeElement(true, true);
515         skipCurrentTreeElement = false;
516     }
517
518     return currentTreeElement;
519 }
520
521 TreeOutline.prototype._searchInputBlur = function(event)
522 {
523     this._searchFinished();
524 }
525
526 TreeOutline.prototype._searchFinished = function()
527 {
528     if (!this._searching)
529         return;
530     
531     delete this._searching;
532     this._childrenListNode.classList.remove("search-match-found");
533     this._childrenListNode.classList.remove("search-match-not-found");
534     delete this._currentSearchMatchElement;
535
536     this.searchInputElement.value = "";
537     this.searchInputElement.removeEventListener("paste", this._boundSearchTextChanged);
538     this.searchInputElement.removeEventListener("cut", this._boundSearchTextChanged);
539     delete this._boundSearchTextChanged;
540
541     this.searchInputElement.removeEventListener("keydown", this._boundSearchInputKeyDown);
542     delete this._boundSearchInputKeyDown;
543     
544     this.searchInputElement.removeEventListener("blur", this._boundSearchInputBlur);
545     delete this._boundSearchInputBlur;
546     
547     if (this.searchFinished)
548         this.searchFinished();
549     
550     this.treeOutline._childrenListNode.focus();
551 }
552
553 TreeOutline.prototype.stopSearch = function()
554 {
555     this._searchFinished();
556 }
557
558 /**
559  * @constructor
560  * @param {Object=} representedObject
561  * @param {boolean=} hasChildren
562  */
563 function TreeElement(title, representedObject, hasChildren)
564 {
565     this._title = title;
566     this.representedObject = (representedObject || {});
567
568     this._hidden = false;
569     this._selectable = true;
570     this.expanded = false;
571     this.selected = false;
572     this.hasChildren = hasChildren;
573     this.children = [];
574     this.treeOutline = null;
575     this.parent = null;
576     this.previousSibling = null;
577     this.nextSibling = null;
578     this._listItemNode = null;
579 }
580
581 TreeElement.prototype = {
582     arrowToggleWidth: 10,
583
584     get selectable() {
585         if (this._hidden)
586             return false;
587         return this._selectable;
588     },
589
590     set selectable(x) {
591         this._selectable = x;
592     },
593
594     get listItemElement() {
595         return this._listItemNode;
596     },
597
598     get childrenListElement() {
599         return this._childrenListNode;
600     },
601
602     get title() {
603         return this._title;
604     },
605
606     set title(x) {
607         this._title = x;
608         this._setListItemNodeContent();
609     },
610
611     get tooltip() {
612         return this._tooltip;
613     },
614
615     set tooltip(x) {
616         this._tooltip = x;
617         if (this._listItemNode)
618             this._listItemNode.title = x ? x : "";
619     },
620
621     get hasChildren() {
622         return this._hasChildren;
623     },
624
625     set hasChildren(x) {
626         if (this._hasChildren === x)
627             return;
628
629         this._hasChildren = x;
630
631         if (!this._listItemNode)
632             return;
633
634         if (x)
635             this._listItemNode.classList.add("parent");
636         else {
637             this._listItemNode.classList.remove("parent");
638             this.collapse();
639         }
640     },
641
642     get hidden() {
643         return this._hidden;
644     },
645
646     set hidden(x) {
647         if (this._hidden === x)
648             return;
649
650         this._hidden = x;
651
652         if (x) {
653             if (this._listItemNode)
654                 this._listItemNode.classList.add("hidden");
655             if (this._childrenListNode)
656                 this._childrenListNode.classList.add("hidden");
657         } else {
658             if (this._listItemNode)
659                 this._listItemNode.classList.remove("hidden");
660             if (this._childrenListNode)
661                 this._childrenListNode.classList.remove("hidden");
662         }
663     },
664
665     get shouldRefreshChildren() {
666         return this._shouldRefreshChildren;
667     },
668
669     set shouldRefreshChildren(x) {
670         this._shouldRefreshChildren = x;
671         if (x && this.expanded)
672             this.expand();
673     },
674
675     _setListItemNodeContent: function()
676     {
677         if (!this._listItemNode)
678             return;
679
680         if (typeof this._title === "string")
681             this._listItemNode.textContent = this._title;
682         else {
683             this._listItemNode.removeChildren();
684             if (this._title)
685                 this._listItemNode.appendChild(this._title);
686         }
687     }
688 }
689
690 TreeElement.prototype.appendChild = TreeOutline.prototype.appendChild;
691 TreeElement.prototype.insertChild = TreeOutline.prototype.insertChild;
692 TreeElement.prototype.removeChild = TreeOutline.prototype.removeChild;
693 TreeElement.prototype.removeChildAtIndex = TreeOutline.prototype.removeChildAtIndex;
694 TreeElement.prototype.removeChildren = TreeOutline.prototype.removeChildren;
695
696 TreeElement.prototype._attach = function()
697 {
698     if (!this._listItemNode || this.parent._shouldRefreshChildren) {
699         if (this._listItemNode && this._listItemNode.parentNode)
700             this._listItemNode.parentNode.removeChild(this._listItemNode);
701
702         this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
703         this._listItemNode.treeElement = this;
704         this._setListItemNodeContent();
705         this._listItemNode.title = this._tooltip ? this._tooltip : "";
706
707         if (this.hidden)
708             this._listItemNode.classList.add("hidden");
709         if (this.hasChildren)
710             this._listItemNode.classList.add("parent");
711         if (this.expanded)
712             this._listItemNode.classList.add("expanded");
713         if (this.selected)
714             this._listItemNode.classList.add("selected");
715
716         this._listItemNode.addEventListener("mousedown", TreeElement.treeElementMouseDown, false);
717         this._listItemNode.addEventListener("click", TreeElement.treeElementToggled, false);
718         this._listItemNode.addEventListener("dblclick", TreeElement.treeElementDoubleClicked, false);
719
720         this.onattach();
721     }
722
723     var nextSibling = null;
724     if (this.nextSibling && this.nextSibling._listItemNode && this.nextSibling._listItemNode.parentNode === this.parent._childrenListNode)
725         nextSibling = this.nextSibling._listItemNode;
726     this.parent._childrenListNode.insertBefore(this._listItemNode, nextSibling);
727     if (this._childrenListNode)
728         this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
729     if (this.selected)
730         this.select();
731     if (this.expanded)
732         this.expand();
733 }
734
735 TreeElement.prototype._detach = function()
736 {
737     if (this._listItemNode && this._listItemNode.parentNode)
738         this._listItemNode.parentNode.removeChild(this._listItemNode);
739     if (this._childrenListNode && this._childrenListNode.parentNode)
740         this._childrenListNode.parentNode.removeChild(this._childrenListNode);
741 }
742
743 TreeElement.treeElementMouseDown = function(event)
744 {
745     var element = event.currentTarget;
746     if (!element || !element.treeElement || !element.treeElement.selectable)
747         return;
748
749     if (element.treeElement.isEventWithinDisclosureTriangle(event))
750         return;
751
752     element.treeElement.selectOnMouseDown(event);
753 }
754
755 TreeElement.treeElementToggled = function(event)
756 {
757     var element = event.currentTarget;
758     if (!element || !element.treeElement)
759         return;
760
761     var toggleOnClick = element.treeElement.toggleOnClick && !element.treeElement.selectable;
762     var isInTriangle = element.treeElement.isEventWithinDisclosureTriangle(event);
763     if (!toggleOnClick && !isInTriangle)
764         return;
765
766     if (element.treeElement.expanded) {
767         if (event.altKey)
768             element.treeElement.collapseRecursively();
769         else
770             element.treeElement.collapse();
771     } else {
772         if (event.altKey)
773             element.treeElement.expandRecursively();
774         else
775             element.treeElement.expand();
776     }
777     event.consume();
778 }
779
780 TreeElement.treeElementDoubleClicked = function(event)
781 {
782     var element = event.currentTarget;
783     if (!element || !element.treeElement)
784         return;
785
786     var handled = element.treeElement.ondblclick.call(element.treeElement, event);
787     if (handled)
788         return;
789     if (element.treeElement.hasChildren && !element.treeElement.expanded)
790         element.treeElement.expand();
791 }
792
793 TreeElement.prototype.collapse = function()
794 {
795     if (this._listItemNode)
796         this._listItemNode.classList.remove("expanded");
797     if (this._childrenListNode)
798         this._childrenListNode.classList.remove("expanded");
799
800     this.expanded = false;
801     
802     if (this.treeOutline)
803         this.treeOutline._expandedStateMap.put(this.representedObject, false);
804
805     this.oncollapse();
806 }
807
808 TreeElement.prototype.collapseRecursively = function()
809 {
810     var item = this;
811     while (item) {
812         if (item.expanded)
813             item.collapse();
814         item = item.traverseNextTreeElement(false, this, true);
815     }
816 }
817
818 TreeElement.prototype.expand = function()
819 {
820     if (!this.hasChildren || (this.expanded && !this._shouldRefreshChildren && this._childrenListNode))
821         return;
822
823     // Set this before onpopulate. Since onpopulate can add elements, this makes
824     // sure the expanded flag is true before calling those functions. This prevents the possibility
825     // of an infinite loop if onpopulate were to call expand.
826
827     this.expanded = true;
828     if (this.treeOutline)
829         this.treeOutline._expandedStateMap.put(this.representedObject, true);
830
831     if (this.treeOutline && (!this._childrenListNode || this._shouldRefreshChildren)) {
832         if (this._childrenListNode && this._childrenListNode.parentNode)
833             this._childrenListNode.parentNode.removeChild(this._childrenListNode);
834
835         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
836         this._childrenListNode.parentTreeElement = this;
837         this._childrenListNode.classList.add("children");
838
839         if (this.hidden)
840             this._childrenListNode.classList.add("hidden");
841
842         this.onpopulate();
843
844         for (var i = 0; i < this.children.length; ++i)
845             this.children[i]._attach();
846
847         delete this._shouldRefreshChildren;
848     }
849
850     if (this._listItemNode) {
851         this._listItemNode.classList.add("expanded");
852         if (this._childrenListNode && this._childrenListNode.parentNode != this._listItemNode.parentNode)
853             this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
854     }
855
856     if (this._childrenListNode)
857         this._childrenListNode.classList.add("expanded");
858
859     this.onexpand();
860 }
861
862 TreeElement.prototype.expandRecursively = function(maxDepth)
863 {
864     var item = this;
865     var info = {};
866     var depth = 0;
867
868     // The Inspector uses TreeOutlines to represents object properties, so recursive expansion
869     // in some case can be infinite, since JavaScript objects can hold circular references.
870     // So default to a recursion cap of 3 levels, since that gives fairly good results.
871     if (isNaN(maxDepth))
872         maxDepth = 3;
873
874     while (item) {
875         if (depth < maxDepth)
876             item.expand();
877         item = item.traverseNextTreeElement(false, this, (depth >= maxDepth), info);
878         depth += info.depthChange;
879     }
880 }
881
882 TreeElement.prototype.hasAncestor = function(ancestor) {
883     if (!ancestor)
884         return false;
885
886     var currentNode = this.parent;
887     while (currentNode) {
888         if (ancestor === currentNode)
889             return true;
890         currentNode = currentNode.parent;
891     }
892
893     return false;
894 }
895
896 TreeElement.prototype.reveal = function()
897 {
898     var currentAncestor = this.parent;
899     while (currentAncestor && !currentAncestor.root) {
900         if (!currentAncestor.expanded)
901             currentAncestor.expand();
902         currentAncestor = currentAncestor.parent;
903     }
904
905     this.onreveal(this);
906 }
907
908 TreeElement.prototype.revealed = function()
909 {
910     var currentAncestor = this.parent;
911     while (currentAncestor && !currentAncestor.root) {
912         if (!currentAncestor.expanded)
913             return false;
914         currentAncestor = currentAncestor.parent;
915     }
916
917     return true;
918 }
919
920 TreeElement.prototype.selectOnMouseDown = function(event)
921 {
922     if (this.select(false, true))
923         event.consume(true);
924 }
925
926 /**
927  * @param {boolean=} omitFocus
928  * @param {boolean=} selectedByUser
929  * @return {boolean}
930  */
931 TreeElement.prototype.select = function(omitFocus, selectedByUser)
932 {
933     if (!this.treeOutline || !this.selectable || this.selected)
934         return false;
935
936     if (this.treeOutline.selectedTreeElement)
937         this.treeOutline.selectedTreeElement.deselect();
938
939     this.selected = true;
940
941     if(!omitFocus)
942         this.treeOutline._childrenListNode.focus();
943
944     // Focusing on another node may detach "this" from tree.
945     if (!this.treeOutline)
946         return false;
947     this.treeOutline.selectedTreeElement = this;
948     if (this._listItemNode)
949         this._listItemNode.classList.add("selected");
950
951     return this.onselect(selectedByUser);
952 }
953
954 /**
955  * @param {boolean=} omitFocus
956  */
957 TreeElement.prototype.revealAndSelect = function(omitFocus)
958 {
959     this.reveal();
960     this.select(omitFocus);
961 }
962
963 /**
964  * @param {boolean=} supressOnDeselect
965  */
966 TreeElement.prototype.deselect = function(supressOnDeselect)
967 {
968     if (!this.treeOutline || this.treeOutline.selectedTreeElement !== this || !this.selected)
969         return false;
970
971     this.selected = false;
972     this.treeOutline.selectedTreeElement = null;
973     if (this._listItemNode)
974         this._listItemNode.classList.remove("selected");
975     return true;
976 }
977
978 // Overridden by subclasses.
979 TreeElement.prototype.onpopulate = function() { }
980 TreeElement.prototype.onenter = function() { }
981 TreeElement.prototype.ondelete = function() { }
982 TreeElement.prototype.onspace = function() { }
983 TreeElement.prototype.onattach = function() { }
984 TreeElement.prototype.onexpand = function() { }
985 TreeElement.prototype.oncollapse = function() { }
986 TreeElement.prototype.ondblclick = function() { }
987 TreeElement.prototype.onreveal = function() { }
988 /** @param {boolean=} selectedByUser */
989 TreeElement.prototype.onselect = function(selectedByUser) { }
990
991 /**
992  * @param {boolean} skipUnrevealed
993  * @param {(TreeOutline|TreeElement)=} stayWithin
994  * @param {boolean=} dontPopulate
995  * @param {Object=} info
996  * @return {TreeElement}
997  */
998 TreeElement.prototype.traverseNextTreeElement = function(skipUnrevealed, stayWithin, dontPopulate, info)
999 {
1000     if (!dontPopulate && this.hasChildren)
1001         this.onpopulate();
1002
1003     if (info)
1004         info.depthChange = 0;
1005
1006     var element = skipUnrevealed ? (this.revealed() ? this.children[0] : null) : this.children[0];
1007     if (element && (!skipUnrevealed || (skipUnrevealed && this.expanded))) {
1008         if (info)
1009             info.depthChange = 1;
1010         return element;
1011     }
1012
1013     if (this === stayWithin)
1014         return null;
1015
1016     element = skipUnrevealed ? (this.revealed() ? this.nextSibling : null) : this.nextSibling;
1017     if (element)
1018         return element;
1019
1020     element = this;
1021     while (element && !element.root && !(skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) && element.parent !== stayWithin) {
1022         if (info)
1023             info.depthChange -= 1;
1024         element = element.parent;
1025     }
1026
1027     if (!element)
1028         return null;
1029
1030     return (skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling);
1031 }
1032
1033 /**
1034  * @param {boolean} skipUnrevealed
1035  * @param {boolean=} dontPopulate
1036  * @return {TreeElement}
1037  */
1038 TreeElement.prototype.traversePreviousTreeElement = function(skipUnrevealed, dontPopulate)
1039 {
1040     var element = skipUnrevealed ? (this.revealed() ? this.previousSibling : null) : this.previousSibling;
1041     if (!dontPopulate && element && element.hasChildren)
1042         element.onpopulate();
1043
1044     while (element && (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1])) {
1045         if (!dontPopulate && element.hasChildren)
1046             element.onpopulate();
1047         element = (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1]);
1048     }
1049
1050     if (element)
1051         return element;
1052
1053     if (!this.parent || this.parent.root)
1054         return null;
1055
1056     return this.parent;
1057 }
1058
1059 TreeElement.prototype.isEventWithinDisclosureTriangle = function(event)
1060 {
1061     // FIXME: We should not use getComputedStyle(). For that we need to get rid of using ::before for disclosure triangle. (http://webk.it/74446) 
1062     var paddingLeftValue = window.getComputedStyle(this._listItemNode).getPropertyCSSValue("padding-left");
1063     var computedLeftPadding = paddingLeftValue ? paddingLeftValue.getFloatValue(CSSPrimitiveValue.CSS_PX) : 0;
1064     var left = this._listItemNode.totalOffsetLeft() + computedLeftPadding;
1065     return event.pageX >= left && event.pageX <= left + this.arrowToggleWidth && this.hasChildren;
1066 }