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