9c52d5c80c029933583290fbf72ec82e35a2a21d
[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     // The representedObject isn't known, so we start at the top of the tree and work down to find the first
249     // tree element that represents representedObject or one of its ancestors.
250     var item;
251     var found = false;
252     for (var i = 0; i < this.children.length; ++i) {
253         item = this.children[i];
254         if (item.representedObject === representedObject || isAncestor(item.representedObject, representedObject)) {
255             found = true;
256             break;
257         }
258     }
259
260     if (!found)
261         return null;
262
263     // Make sure the item that we found is connected to the root of the tree.
264     // Build up a list of representedObject's ancestors that aren't already in our tree.
265     var ancestors = [];
266     var currentObject = representedObject;
267     while (currentObject) {
268         ancestors.unshift(currentObject);
269         if (currentObject === item.representedObject)
270             break;
271         currentObject = getParent(currentObject);
272     }
273
274     // For each of those ancestors we populate them to fill in the tree.
275     for (var i = 0; i < ancestors.length; ++i) {
276         // Make sure we don't call findTreeElement with the same representedObject
277         // again, to prevent infinite recursion.
278         if (ancestors[i] === representedObject)
279             continue;
280         // FIXME: we could do something faster than findTreeElement since we will know the next
281         // ancestor exists in the tree.
282         item = this.findTreeElement(ancestors[i], isAncestor, getParent);
283         if (item)
284             item.onpopulate();
285     }
286
287     return this.getCachedTreeElement(representedObject);
288 }
289
290 TreeOutline.prototype.treeElementFromPoint = function(x, y)
291 {
292     var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y);
293     if (!node)
294         return null;
295
296     var listNode = node.enclosingNodeOrSelfWithNodeNameInArray(["ol", "li"]);
297     if (listNode)
298         return listNode.parentTreeElement || listNode.treeElement;
299     return null;
300 }
301
302 TreeOutline.prototype._treeKeyPress = function(event)
303 {
304     if (!this.searchable || WebInspector.isBeingEdited(this._childrenListNode))
305         return;
306     
307     var searchText = String.fromCharCode(event.charCode);
308     // Ignore whitespace.
309     if (searchText.trim() !== searchText)
310         return;
311
312     this._startSearch(searchText);
313     event.consume(true);
314 }
315
316 TreeOutline.prototype._treeKeyDown = function(event)
317 {
318     if (event.target !== this._childrenListNode)
319         return;
320
321     if (!this.selectedTreeElement || event.shiftKey || event.metaKey || event.ctrlKey)
322         return;
323
324     var handled = false;
325     var nextSelectedElement;
326     if (event.keyIdentifier === "Up" && !event.altKey) {
327         nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true);
328         while (nextSelectedElement && !nextSelectedElement.selectable)
329             nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(!this.expandTreeElementsWhenArrowing);
330         handled = nextSelectedElement ? true : false;
331     } else if (event.keyIdentifier === "Down" && !event.altKey) {
332         nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true);
333         while (nextSelectedElement && !nextSelectedElement.selectable)
334             nextSelectedElement = nextSelectedElement.traverseNextTreeElement(!this.expandTreeElementsWhenArrowing);
335         handled = nextSelectedElement ? true : false;
336     } else if (event.keyIdentifier === "Left") {
337         if (this.selectedTreeElement.expanded) {
338             if (event.altKey)
339                 this.selectedTreeElement.collapseRecursively();
340             else
341                 this.selectedTreeElement.collapse();
342             handled = true;
343         } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) {
344             handled = true;
345             if (this.selectedTreeElement.parent.selectable) {
346                 nextSelectedElement = this.selectedTreeElement.parent;
347                 while (nextSelectedElement && !nextSelectedElement.selectable)
348                     nextSelectedElement = nextSelectedElement.parent;
349                 handled = nextSelectedElement ? true : false;
350             } else if (this.selectedTreeElement.parent)
351                 this.selectedTreeElement.parent.collapse();
352         }
353     } else if (event.keyIdentifier === "Right") {
354         if (!this.selectedTreeElement.revealed()) {
355             this.selectedTreeElement.reveal();
356             handled = true;
357         } else if (this.selectedTreeElement.hasChildren) {
358             handled = true;
359             if (this.selectedTreeElement.expanded) {
360                 nextSelectedElement = this.selectedTreeElement.children[0];
361                 while (nextSelectedElement && !nextSelectedElement.selectable)
362                     nextSelectedElement = nextSelectedElement.nextSibling;
363                 handled = nextSelectedElement ? true : false;
364             } else {
365                 if (event.altKey)
366                     this.selectedTreeElement.expandRecursively();
367                 else
368                     this.selectedTreeElement.expand();
369             }
370         }
371     } else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */)
372         handled = this.selectedTreeElement.ondelete();
373     else if (isEnterKey(event))
374         handled = this.selectedTreeElement.onenter();
375     else if (event.keyCode === WebInspector.KeyboardShortcut.Keys.Space.code)
376         handled = this.selectedTreeElement.onspace();
377
378     if (nextSelectedElement) {
379         nextSelectedElement.reveal();
380         nextSelectedElement.select(false, true);
381     }
382
383     if (handled)
384         event.consume(true);
385 }
386
387 TreeOutline.prototype.expand = function()
388 {
389     // this is the root, do nothing
390 }
391
392 TreeOutline.prototype.collapse = function()
393 {
394     // this is the root, do nothing
395 }
396
397 TreeOutline.prototype.revealed = function()
398 {
399     return true;
400 }
401
402 TreeOutline.prototype.reveal = function()
403 {
404     // this is the root, do nothing
405 }
406
407 TreeOutline.prototype.select = function()
408 {
409     // this is the root, do nothing
410 }
411
412 /**
413  * @param {boolean=} omitFocus
414  */
415 TreeOutline.prototype.revealAndSelect = function(omitFocus)
416 {
417     // this is the root, do nothing
418 }
419
420 /**
421  * @param {string} searchText
422  */
423 TreeOutline.prototype._startSearch = function(searchText)
424 {
425     if (!this.searchInputElement || !this.searchable)
426         return;
427     
428     this._searching = true;
429
430     if (this.searchStarted)
431         this.searchStarted();
432     
433     this.searchInputElement.value = searchText;
434     
435     function focusSearchInput()
436     {
437         this.searchInputElement.focus();
438     }
439     window.setTimeout(focusSearchInput.bind(this), 0);
440     this._searchTextChanged();
441     this._boundSearchTextChanged = this._searchTextChanged.bind(this);
442     this.searchInputElement.addEventListener("paste", this._boundSearchTextChanged);
443     this.searchInputElement.addEventListener("cut", this._boundSearchTextChanged);
444     this.searchInputElement.addEventListener("keypress", this._boundSearchTextChanged);
445     this._boundSearchInputKeyDown = this._searchInputKeyDown.bind(this);
446     this.searchInputElement.addEventListener("keydown", this._boundSearchInputKeyDown);
447     this._boundSearchInputBlur = this._searchInputBlur.bind(this);
448     this.searchInputElement.addEventListener("blur", this._boundSearchInputBlur);
449 }
450
451 TreeOutline.prototype._searchTextChanged = function()
452 {
453     function updateSearch()
454     {
455         var nextSelectedElement = this._nextSearchMatch(this.searchInputElement.value, this.selectedTreeElement, false);
456         if (!nextSelectedElement)
457             nextSelectedElement = this._nextSearchMatch(this.searchInputElement.value, this.children[0], false);
458         this._showSearchMatchElement(nextSelectedElement);
459     }
460     
461     window.setTimeout(updateSearch.bind(this), 0);
462 }
463
464 TreeOutline.prototype._showSearchMatchElement = function(treeElement)
465 {
466     this._currentSearchMatchElement = treeElement;
467     if (treeElement) {
468         this._childrenListNode.classList.add("search-match-found");
469         this._childrenListNode.classList.remove("search-match-not-found");
470         treeElement.revealAndSelect(true);
471     } else {
472         this._childrenListNode.classList.remove("search-match-found");
473         this._childrenListNode.classList.add("search-match-not-found");
474     }
475 }
476
477 TreeOutline.prototype._searchInputKeyDown = function(event)
478 {
479     if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey)
480         return;
481
482     var handled = false;
483     var nextSelectedElement;
484     if (event.keyIdentifier === "Down") {
485         nextSelectedElement = this._nextSearchMatch(this.searchInputElement.value, this.selectedTreeElement, true);
486         handled = true;
487     } else if (event.keyIdentifier === "Up") {
488         nextSelectedElement = this._previousSearchMatch(this.searchInputElement.value, this.selectedTreeElement);
489         handled = true;
490     } else if (event.keyCode === 27 /* Esc */) {
491         this._searchFinished();
492         handled = true;
493     } else if (isEnterKey(event)) {
494         var lastSearchMatchElement = this._currentSearchMatchElement;
495         this._searchFinished();
496         lastSearchMatchElement.onenter();
497         handled = true;
498     }
499
500     if (nextSelectedElement)
501         this._showSearchMatchElement(nextSelectedElement);
502         
503     if (handled)
504         event.consume(true);
505     else
506        window.setTimeout(this._boundSearchTextChanged, 0); 
507 }
508
509 /**
510  * @param {string} searchText
511  * @param {TreeElement} startTreeElement
512  * @param {boolean} skipStartTreeElement
513  */
514 TreeOutline.prototype._nextSearchMatch = function(searchText, startTreeElement, skipStartTreeElement)
515 {
516     var currentTreeElement = startTreeElement;
517     var skipCurrentTreeElement = skipStartTreeElement;
518     while (currentTreeElement && (skipCurrentTreeElement || !currentTreeElement.matchesSearchText || !currentTreeElement.matchesSearchText(searchText))) {
519         currentTreeElement = currentTreeElement.traverseNextTreeElement(true, null, true);
520         skipCurrentTreeElement = false;
521     }
522
523     return currentTreeElement;
524 }
525
526 /**
527  * @param {string} searchText
528  * @param {TreeElement=} startTreeElement
529  */
530 TreeOutline.prototype._previousSearchMatch = function(searchText, startTreeElement)
531 {
532     var currentTreeElement = startTreeElement;
533     var skipCurrentTreeElement = true;
534     while (currentTreeElement && (skipCurrentTreeElement || !currentTreeElement.matchesSearchText || !currentTreeElement.matchesSearchText(searchText))) {
535         currentTreeElement = currentTreeElement.traversePreviousTreeElement(true, true);
536         skipCurrentTreeElement = false;
537     }
538
539     return currentTreeElement;
540 }
541
542 TreeOutline.prototype._searchInputBlur = function(event)
543 {
544     this._searchFinished();
545 }
546
547 TreeOutline.prototype._searchFinished = function()
548 {
549     if (!this._searching)
550         return;
551     
552     delete this._searching;
553     this._childrenListNode.classList.remove("search-match-found");
554     this._childrenListNode.classList.remove("search-match-not-found");
555     delete this._currentSearchMatchElement;
556
557     this.searchInputElement.value = "";
558     this.searchInputElement.removeEventListener("paste", this._boundSearchTextChanged);
559     this.searchInputElement.removeEventListener("cut", this._boundSearchTextChanged);
560     delete this._boundSearchTextChanged;
561
562     this.searchInputElement.removeEventListener("keydown", this._boundSearchInputKeyDown);
563     delete this._boundSearchInputKeyDown;
564     
565     this.searchInputElement.removeEventListener("blur", this._boundSearchInputBlur);
566     delete this._boundSearchInputBlur;
567     
568     if (this.searchFinished)
569         this.searchFinished();
570     
571     this.treeOutline._childrenListNode.focus();
572 }
573
574 TreeOutline.prototype.stopSearch = function()
575 {
576     this._searchFinished();
577 }
578
579 /**
580  * @constructor
581  * @param {Object=} representedObject
582  * @param {boolean=} hasChildren
583  */
584 function TreeElement(title, representedObject, hasChildren)
585 {
586     this._title = title;
587     this.representedObject = (representedObject || {});
588
589     this._hidden = false;
590     this._selectable = true;
591     this.expanded = false;
592     this.selected = false;
593     this.hasChildren = hasChildren;
594     this.children = [];
595     this.treeOutline = null;
596     this.parent = null;
597     this.previousSibling = null;
598     this.nextSibling = null;
599     this._listItemNode = null;
600 }
601
602 TreeElement.prototype = {
603     arrowToggleWidth: 10,
604
605     get selectable() {
606         if (this._hidden)
607             return false;
608         return this._selectable;
609     },
610
611     set selectable(x) {
612         this._selectable = x;
613     },
614
615     get listItemElement() {
616         return this._listItemNode;
617     },
618
619     get childrenListElement() {
620         return this._childrenListNode;
621     },
622
623     get title() {
624         return this._title;
625     },
626
627     set title(x) {
628         this._title = x;
629         this._setListItemNodeContent();
630     },
631
632     get tooltip() {
633         return this._tooltip;
634     },
635
636     set tooltip(x) {
637         this._tooltip = x;
638         if (this._listItemNode)
639             this._listItemNode.title = x ? x : "";
640     },
641
642     get hasChildren() {
643         return this._hasChildren;
644     },
645
646     set hasChildren(x) {
647         if (this._hasChildren === x)
648             return;
649
650         this._hasChildren = x;
651
652         if (!this._listItemNode)
653             return;
654
655         if (x)
656             this._listItemNode.classList.add("parent");
657         else {
658             this._listItemNode.classList.remove("parent");
659             this.collapse();
660         }
661     },
662
663     get hidden() {
664         return this._hidden;
665     },
666
667     set hidden(x) {
668         if (this._hidden === x)
669             return;
670
671         this._hidden = x;
672
673         if (x) {
674             if (this._listItemNode)
675                 this._listItemNode.classList.add("hidden");
676             if (this._childrenListNode)
677                 this._childrenListNode.classList.add("hidden");
678         } else {
679             if (this._listItemNode)
680                 this._listItemNode.classList.remove("hidden");
681             if (this._childrenListNode)
682                 this._childrenListNode.classList.remove("hidden");
683         }
684     },
685
686     get shouldRefreshChildren() {
687         return this._shouldRefreshChildren;
688     },
689
690     set shouldRefreshChildren(x) {
691         this._shouldRefreshChildren = x;
692         if (x && this.expanded)
693             this.expand();
694     },
695
696     _setListItemNodeContent: function()
697     {
698         if (!this._listItemNode)
699             return;
700
701         if (typeof this._title === "string")
702             this._listItemNode.textContent = this._title;
703         else {
704             this._listItemNode.removeChildren();
705             if (this._title)
706                 this._listItemNode.appendChild(this._title);
707         }
708     }
709 }
710
711 TreeElement.prototype.appendChild = TreeOutline.prototype.appendChild;
712 TreeElement.prototype.insertChild = TreeOutline.prototype.insertChild;
713 TreeElement.prototype.removeChild = TreeOutline.prototype.removeChild;
714 TreeElement.prototype.removeChildAtIndex = TreeOutline.prototype.removeChildAtIndex;
715 TreeElement.prototype.removeChildren = TreeOutline.prototype.removeChildren;
716
717 TreeElement.prototype._attach = function()
718 {
719     if (!this._listItemNode || this.parent._shouldRefreshChildren) {
720         if (this._listItemNode && this._listItemNode.parentNode)
721             this._listItemNode.parentNode.removeChild(this._listItemNode);
722
723         this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
724         this._listItemNode.treeElement = this;
725         this._setListItemNodeContent();
726         this._listItemNode.title = this._tooltip ? this._tooltip : "";
727
728         if (this.hidden)
729             this._listItemNode.classList.add("hidden");
730         if (this.hasChildren)
731             this._listItemNode.classList.add("parent");
732         if (this.expanded)
733             this._listItemNode.classList.add("expanded");
734         if (this.selected)
735             this._listItemNode.classList.add("selected");
736
737         this._listItemNode.addEventListener("mousedown", TreeElement.treeElementMouseDown, false);
738         this._listItemNode.addEventListener("click", TreeElement.treeElementToggled, false);
739         this._listItemNode.addEventListener("dblclick", TreeElement.treeElementDoubleClicked, false);
740
741         this.onattach();
742     }
743
744     var nextSibling = null;
745     if (this.nextSibling && this.nextSibling._listItemNode && this.nextSibling._listItemNode.parentNode === this.parent._childrenListNode)
746         nextSibling = this.nextSibling._listItemNode;
747     this.parent._childrenListNode.insertBefore(this._listItemNode, nextSibling);
748     if (this._childrenListNode)
749         this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
750     if (this.selected)
751         this.select();
752     if (this.expanded)
753         this.expand();
754 }
755
756 TreeElement.prototype._detach = function()
757 {
758     if (this._listItemNode && this._listItemNode.parentNode)
759         this._listItemNode.parentNode.removeChild(this._listItemNode);
760     if (this._childrenListNode && this._childrenListNode.parentNode)
761         this._childrenListNode.parentNode.removeChild(this._childrenListNode);
762 }
763
764 TreeElement.treeElementMouseDown = function(event)
765 {
766     var element = event.currentTarget;
767     if (!element || !element.treeElement || !element.treeElement.selectable)
768         return;
769
770     if (element.treeElement.isEventWithinDisclosureTriangle(event))
771         return;
772
773     element.treeElement.selectOnMouseDown(event);
774 }
775
776 TreeElement.treeElementToggled = function(event)
777 {
778     var element = event.currentTarget;
779     if (!element || !element.treeElement)
780         return;
781
782     var toggleOnClick = element.treeElement.toggleOnClick && !element.treeElement.selectable;
783     var isInTriangle = element.treeElement.isEventWithinDisclosureTriangle(event);
784     if (!toggleOnClick && !isInTriangle)
785         return;
786
787     if (element.treeElement.expanded) {
788         if (event.altKey)
789             element.treeElement.collapseRecursively();
790         else
791             element.treeElement.collapse();
792     } else {
793         if (event.altKey)
794             element.treeElement.expandRecursively();
795         else
796             element.treeElement.expand();
797     }
798     event.consume();
799 }
800
801 TreeElement.treeElementDoubleClicked = function(event)
802 {
803     var element = event.currentTarget;
804     if (!element || !element.treeElement)
805         return;
806
807     var handled = element.treeElement.ondblclick.call(element.treeElement, event);
808     if (handled)
809         return;
810     if (element.treeElement.hasChildren && !element.treeElement.expanded)
811         element.treeElement.expand();
812 }
813
814 TreeElement.prototype.collapse = function()
815 {
816     if (this._listItemNode)
817         this._listItemNode.classList.remove("expanded");
818     if (this._childrenListNode)
819         this._childrenListNode.classList.remove("expanded");
820
821     this.expanded = false;
822     
823     if (this.treeOutline)
824         this.treeOutline._expandedStateMap.put(this.representedObject, false);
825
826     this.oncollapse();
827 }
828
829 TreeElement.prototype.collapseRecursively = function()
830 {
831     var item = this;
832     while (item) {
833         if (item.expanded)
834             item.collapse();
835         item = item.traverseNextTreeElement(false, this, true);
836     }
837 }
838
839 TreeElement.prototype.expand = function()
840 {
841     if (!this.hasChildren || (this.expanded && !this._shouldRefreshChildren && this._childrenListNode))
842         return;
843
844     // Set this before onpopulate. Since onpopulate can add elements, this makes
845     // sure the expanded flag is true before calling those functions. This prevents the possibility
846     // of an infinite loop if onpopulate were to call expand.
847
848     this.expanded = true;
849     if (this.treeOutline)
850         this.treeOutline._expandedStateMap.put(this.representedObject, true);
851
852     if (this.treeOutline && (!this._childrenListNode || this._shouldRefreshChildren)) {
853         if (this._childrenListNode && this._childrenListNode.parentNode)
854             this._childrenListNode.parentNode.removeChild(this._childrenListNode);
855
856         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
857         this._childrenListNode.parentTreeElement = this;
858         this._childrenListNode.classList.add("children");
859
860         if (this.hidden)
861             this._childrenListNode.classList.add("hidden");
862
863         this.onpopulate();
864
865         for (var i = 0; i < this.children.length; ++i)
866             this.children[i]._attach();
867
868         delete this._shouldRefreshChildren;
869     }
870
871     if (this._listItemNode) {
872         this._listItemNode.classList.add("expanded");
873         if (this._childrenListNode && this._childrenListNode.parentNode != this._listItemNode.parentNode)
874             this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
875     }
876
877     if (this._childrenListNode)
878         this._childrenListNode.classList.add("expanded");
879
880     this.onexpand();
881 }
882
883 TreeElement.prototype.expandRecursively = function(maxDepth)
884 {
885     var item = this;
886     var info = {};
887     var depth = 0;
888
889     // The Inspector uses TreeOutlines to represents object properties, so recursive expansion
890     // in some case can be infinite, since JavaScript objects can hold circular references.
891     // So default to a recursion cap of 3 levels, since that gives fairly good results.
892     if (isNaN(maxDepth))
893         maxDepth = 3;
894
895     while (item) {
896         if (depth < maxDepth)
897             item.expand();
898         item = item.traverseNextTreeElement(false, this, (depth >= maxDepth), info);
899         depth += info.depthChange;
900     }
901 }
902
903 TreeElement.prototype.hasAncestor = function(ancestor) {
904     if (!ancestor)
905         return false;
906
907     var currentNode = this.parent;
908     while (currentNode) {
909         if (ancestor === currentNode)
910             return true;
911         currentNode = currentNode.parent;
912     }
913
914     return false;
915 }
916
917 TreeElement.prototype.reveal = function()
918 {
919     var currentAncestor = this.parent;
920     while (currentAncestor && !currentAncestor.root) {
921         if (!currentAncestor.expanded)
922             currentAncestor.expand();
923         currentAncestor = currentAncestor.parent;
924     }
925
926     this.onreveal(this);
927 }
928
929 TreeElement.prototype.revealed = function()
930 {
931     var currentAncestor = this.parent;
932     while (currentAncestor && !currentAncestor.root) {
933         if (!currentAncestor.expanded)
934             return false;
935         currentAncestor = currentAncestor.parent;
936     }
937
938     return true;
939 }
940
941 TreeElement.prototype.selectOnMouseDown = function(event)
942 {
943     if (this.select(false, true))
944         event.consume(true);
945 }
946
947 /**
948  * @param {boolean=} omitFocus
949  * @param {boolean=} selectedByUser
950  * @return {boolean}
951  */
952 TreeElement.prototype.select = function(omitFocus, selectedByUser)
953 {
954     if (!this.treeOutline || !this.selectable || this.selected)
955         return false;
956
957     if (this.treeOutline.selectedTreeElement)
958         this.treeOutline.selectedTreeElement.deselect();
959
960     this.selected = true;
961
962     if(!omitFocus)
963         this.treeOutline._childrenListNode.focus();
964
965     // Focusing on another node may detach "this" from tree.
966     if (!this.treeOutline)
967         return false;
968     this.treeOutline.selectedTreeElement = this;
969     if (this._listItemNode)
970         this._listItemNode.classList.add("selected");
971
972     return this.onselect(selectedByUser);
973 }
974
975 /**
976  * @param {boolean=} omitFocus
977  */
978 TreeElement.prototype.revealAndSelect = function(omitFocus)
979 {
980     this.reveal();
981     this.select(omitFocus);
982 }
983
984 /**
985  * @param {boolean=} supressOnDeselect
986  */
987 TreeElement.prototype.deselect = function(supressOnDeselect)
988 {
989     if (!this.treeOutline || this.treeOutline.selectedTreeElement !== this || !this.selected)
990         return false;
991
992     this.selected = false;
993     this.treeOutline.selectedTreeElement = null;
994     if (this._listItemNode)
995         this._listItemNode.classList.remove("selected");
996     return true;
997 }
998
999 // Overridden by subclasses.
1000 TreeElement.prototype.onpopulate = function() { }
1001 TreeElement.prototype.onenter = function() { }
1002 TreeElement.prototype.ondelete = function() { }
1003 TreeElement.prototype.onspace = function() { }
1004 TreeElement.prototype.onattach = function() { }
1005 TreeElement.prototype.onexpand = function() { }
1006 TreeElement.prototype.oncollapse = function() { }
1007 TreeElement.prototype.ondblclick = function() { }
1008 TreeElement.prototype.onreveal = function() { }
1009 /** @param {boolean=} selectedByUser */
1010 TreeElement.prototype.onselect = function(selectedByUser) { }
1011
1012 /**
1013  * @param {boolean} skipUnrevealed
1014  * @param {(TreeOutline|TreeElement)=} stayWithin
1015  * @param {boolean=} dontPopulate
1016  * @param {Object=} info
1017  * @return {TreeElement}
1018  */
1019 TreeElement.prototype.traverseNextTreeElement = function(skipUnrevealed, stayWithin, dontPopulate, info)
1020 {
1021     if (!dontPopulate && this.hasChildren)
1022         this.onpopulate();
1023
1024     if (info)
1025         info.depthChange = 0;
1026
1027     var element = skipUnrevealed ? (this.revealed() ? this.children[0] : null) : this.children[0];
1028     if (element && (!skipUnrevealed || (skipUnrevealed && this.expanded))) {
1029         if (info)
1030             info.depthChange = 1;
1031         return element;
1032     }
1033
1034     if (this === stayWithin)
1035         return null;
1036
1037     element = skipUnrevealed ? (this.revealed() ? this.nextSibling : null) : this.nextSibling;
1038     if (element)
1039         return element;
1040
1041     element = this;
1042     while (element && !element.root && !(skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) && element.parent !== stayWithin) {
1043         if (info)
1044             info.depthChange -= 1;
1045         element = element.parent;
1046     }
1047
1048     if (!element)
1049         return null;
1050
1051     return (skipUnrevealed ? (element.revealed() ? element.nextSibling : null) : element.nextSibling);
1052 }
1053
1054 /**
1055  * @param {boolean} skipUnrevealed
1056  * @param {boolean=} dontPopulate
1057  * @return {TreeElement}
1058  */
1059 TreeElement.prototype.traversePreviousTreeElement = function(skipUnrevealed, dontPopulate)
1060 {
1061     var element = skipUnrevealed ? (this.revealed() ? this.previousSibling : null) : this.previousSibling;
1062     if (!dontPopulate && element && element.hasChildren)
1063         element.onpopulate();
1064
1065     while (element && (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1])) {
1066         if (!dontPopulate && element.hasChildren)
1067             element.onpopulate();
1068         element = (skipUnrevealed ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1]);
1069     }
1070
1071     if (element)
1072         return element;
1073
1074     if (!this.parent || this.parent.root)
1075         return null;
1076
1077     return this.parent;
1078 }
1079
1080 TreeElement.prototype.isEventWithinDisclosureTriangle = function(event)
1081 {
1082     // FIXME: We should not use getComputedStyle(). For that we need to get rid of using ::before for disclosure triangle. (http://webk.it/74446) 
1083     var paddingLeftValue = window.getComputedStyle(this._listItemNode).getPropertyCSSValue("padding-left");
1084     var computedLeftPadding = paddingLeftValue ? paddingLeftValue.getFloatValue(CSSPrimitiveValue.CSS_PX) : 0;
1085     var left = this._listItemNode.totalOffsetLeft() + computedLeftPadding;
1086     return event.pageX >= left && event.pageX <= left + this.arrowToggleWidth && this.hasChildren;
1087 }