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