treeoutline.js revision 643ca7872b450ea4efacab6188849e5aac2ba161
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
29function TreeOutline(listNode)
30{
31    this.children = [];
32    this.selectedTreeElement = null;
33    this._childrenListNode = listNode;
34    this._childrenListNode.removeChildren();
35    this._knownTreeElements = [];
36    this._treeElementsExpandedState = [];
37    this.expandTreeElementsWhenArrowing = false;
38    this.root = true;
39    this.hasChildren = false;
40    this.expanded = true;
41    this.selected = false;
42    this.treeOutline = this;
43}
44
45TreeOutline._knownTreeElementNextIdentifier = 1;
46
47TreeOutline._appendChild = function(child)
48{
49    if (!child)
50        throw("child can't be undefined or null");
51
52    var lastChild = this.children[this.children.length - 1];
53    if (lastChild) {
54        lastChild.nextSibling = child;
55        child.previousSibling = lastChild;
56    } else {
57        child.previousSibling = null;
58        child.nextSibling = null;
59    }
60
61    this.children.push(child);
62    this.hasChildren = true;
63    child.parent = this;
64    child.treeOutline = this.treeOutline;
65    child.treeOutline._rememberTreeElement(child);
66
67    var current = child.children[0];
68    while (current) {
69        current.treeOutline = this.treeOutline;
70        current.treeOutline._rememberTreeElement(current);
71        current = current.traverseNextTreeElement(false, child, true);
72    }
73
74    if (child.hasChildren && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined)
75        child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier];
76
77    if (!this._childrenListNode) {
78        this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
79        this._childrenListNode.parentTreeElement = this;
80        this._childrenListNode.addStyleClass("children");
81        if (this.hidden)
82            this._childrenListNode.addStyleClass("hidden");
83    }
84
85    child._attach();
86}
87
88TreeOutline._insertChild = function(child, index)
89{
90    if (!child)
91        throw("child can't be undefined or null");
92
93    var previousChild = (index > 0 ? this.children[index - 1] : null);
94    if (previousChild) {
95        previousChild.nextSibling = child;
96        child.previousSibling = previousChild;
97    } else {
98        child.previousSibling = null;
99    }
100
101    var nextChild = this.children[index];
102    if (nextChild) {
103        nextChild.previousSibling = child;
104        child.nextSibling = nextChild;
105    } else {
106        child.nextSibling = null;
107    }
108
109    this.children.splice(index, 0, child);
110    this.hasChildren = true;
111    child.parent = this;
112    child.treeOutline = this.treeOutline;
113    child.treeOutline._rememberTreeElement(child);
114
115    var current = child.children[0];
116    while (current) {
117        current.treeOutline = this.treeOutline;
118        current.treeOutline._rememberTreeElement(current);
119        current = current.traverseNextTreeElement(false, child, true);
120    }
121
122    if (child.hasChildren && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined)
123        child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier];
124
125    if (!this._childrenListNode) {
126        this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
127        this._childrenListNode.parentTreeElement = this;
128        this._childrenListNode.addStyleClass("children");
129        if (this.hidden)
130            this._childrenListNode.addStyleClass("hidden");
131    }
132
133    child._attach();
134}
135
136TreeOutline._removeChildAtIndex = function(childIndex)
137{
138    if (childIndex < 0 || childIndex >= this.children.length)
139        throw("childIndex out of range");
140
141    var child = this.children[childIndex];
142    this.children.splice(childIndex, 1);
143
144    child.deselect();
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._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    TreeOutline._removeChildAtIndex.call(this, childIndex);
173}
174
175TreeOutline._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._removeChildrenRecursive = function()
197{
198    var childrenToRemove = this.children;
199
200    var child = this.children[0];
201    while (child) {
202        if (child.children.length)
203            childrenToRemove = childrenToRemove.concat(child.children);
204        child = child.traverseNextTreeElement(false, this, true);
205    }
206
207    for (var i = 0; i < childrenToRemove.length; ++i) {
208        var child = childrenToRemove[i];
209        child.deselect();
210        if (child.treeOutline)
211            child.treeOutline._forgetTreeElement(child);
212        child._detach();
213        child.children = [];
214        child.treeOutline = null;
215        child.parent = null;
216        child.nextSibling = null;
217        child.previousSibling = null;
218    }
219
220    this.children = [];
221}
222
223TreeOutline.prototype._rememberTreeElement = function(element)
224{
225    if (!this._knownTreeElements[element.identifier])
226        this._knownTreeElements[element.identifier] = [];
227
228    // check if the element is already known
229    var elements = this._knownTreeElements[element.identifier];
230    if (elements.indexOf(element) !== -1)
231        return;
232
233    // add the element
234    elements.push(element);
235}
236
237TreeOutline.prototype._forgetTreeElement = function(element)
238{
239    if (this._knownTreeElements[element.identifier])
240        this._knownTreeElements[element.identifier].remove(element, true);
241}
242
243TreeOutline.prototype._forgetChildrenRecursive = function(parentElement)
244{
245    var child = parentElement.children[0];
246    while (child) {
247        this._forgetTreeElement(child);
248        child = child.traverseNextTreeElement(false, this, true);
249    }
250}
251
252TreeOutline.prototype.getCachedTreeElement = function(representedObject)
253{
254    if (!representedObject)
255        return null;
256
257    if ("__treeElementIdentifier" in representedObject) {
258        // If this representedObject has a tree element identifier, and it is a known TreeElement
259        // in our tree we can just return that tree element.
260        var elements = this._knownTreeElements[representedObject.__treeElementIdentifier];
261        if (elements) {
262            for (var i = 0; i < elements.length; ++i)
263                if (elements[i].representedObject === representedObject)
264                    return elements[i];
265        }
266    }
267    return null;
268}
269
270TreeOutline.prototype.findTreeElement = function(representedObject, isAncestor, getParent)
271{
272    if (!representedObject)
273        return null;
274
275    var cachedElement = this.getCachedTreeElement(representedObject);
276    if (cachedElement)
277        return cachedElement;
278
279    // The representedObject isn't know, so we start at the top of the tree and work down to find the first
280    // tree element that represents representedObject or one of its ancestors.
281    var item;
282    var found = false;
283    for (var i = 0; i < this.children.length; ++i) {
284        item = this.children[i];
285        if (item.representedObject === representedObject || isAncestor(item.representedObject, representedObject)) {
286            found = true;
287            break;
288        }
289    }
290
291    if (!found)
292        return null;
293
294    // Make sure the item that we found is connected to the root of the tree.
295    // Build up a list of representedObject's ancestors that aren't already in our tree.
296    var ancestors = [];
297    var currentObject = representedObject;
298    while (currentObject) {
299        ancestors.unshift(currentObject);
300        if (currentObject === item.representedObject)
301            break;
302        currentObject = getParent(currentObject);
303    }
304
305    // For each of those ancestors we populate them to fill in the tree.
306    for (var i = 0; i < ancestors.length; ++i) {
307        // Make sure we don't call findTreeElement with the same representedObject
308        // again, to prevent infinite recursion.
309        if (ancestors[i] === representedObject)
310            continue;
311        // FIXME: we could do something faster than findTreeElement since we will know the next
312        // ancestor exists in the tree.
313        item = this.findTreeElement(ancestors[i], isAncestor, getParent);
314        if (item && item.onpopulate)
315            item.onpopulate(item);
316    }
317
318    return this.getCachedTreeElement(representedObject);
319}
320
321TreeOutline.prototype.treeElementFromPoint = function(x, y)
322{
323    var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y);
324    var listNode = node.enclosingNodeOrSelfWithNodeNameInArray(["ol", "li"]);
325    if (listNode)
326        return listNode.parentTreeElement || listNode.treeElement;
327    return null;
328}
329
330TreeOutline.prototype.handleKeyEvent = function(event)
331{
332    if (!this.selectedTreeElement || event.shiftKey || event.metaKey || event.ctrlKey)
333        return false;
334
335    var handled = false;
336    var nextSelectedElement;
337    if (event.keyIdentifier === "Up" && !event.altKey) {
338        nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true);
339        while (nextSelectedElement && !nextSelectedElement.selectable)
340            nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(!this.expandTreeElementsWhenArrowing);
341        handled = nextSelectedElement ? true : false;
342    } else if (event.keyIdentifier === "Down" && !event.altKey) {
343        nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true);
344        while (nextSelectedElement && !nextSelectedElement.selectable)
345            nextSelectedElement = nextSelectedElement.traverseNextTreeElement(!this.expandTreeElementsWhenArrowing);
346        handled = nextSelectedElement ? true : false;
347    } else if (event.keyIdentifier === "Left") {
348        if (this.selectedTreeElement.expanded) {
349            if (event.altKey)
350                this.selectedTreeElement.collapseRecursively();
351            else
352                this.selectedTreeElement.collapse();
353            handled = true;
354        } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) {
355            handled = true;
356            if (this.selectedTreeElement.parent.selectable) {
357                nextSelectedElement = this.selectedTreeElement.parent;
358                handled = nextSelectedElement ? true : false;
359            } else if (this.selectedTreeElement.parent)
360                this.selectedTreeElement.parent.collapse();
361        }
362    } else if (event.keyIdentifier === "Right") {
363        if (!this.selectedTreeElement.revealed()) {
364            this.selectedTreeElement.reveal();
365            handled = true;
366        } else if (this.selectedTreeElement.hasChildren) {
367            handled = true;
368            if (this.selectedTreeElement.expanded) {
369                nextSelectedElement = this.selectedTreeElement.children[0];
370                handled = nextSelectedElement ? true : false;
371            } else {
372                if (event.altKey)
373                    this.selectedTreeElement.expandRecursively();
374                else
375                    this.selectedTreeElement.expand();
376            }
377        }
378    }
379
380    if (nextSelectedElement) {
381        nextSelectedElement.reveal();
382        nextSelectedElement.select();
383    }
384
385    if (handled) {
386        event.preventDefault();
387        event.stopPropagation();
388    }
389
390    return handled;
391}
392
393TreeOutline.prototype.expand = function()
394{
395    // this is the root, do nothing
396}
397
398TreeOutline.prototype.collapse = function()
399{
400    // this is the root, do nothing
401}
402
403TreeOutline.prototype.revealed = function()
404{
405    return true;
406}
407
408TreeOutline.prototype.reveal = function()
409{
410    // this is the root, do nothing
411}
412
413TreeOutline.prototype.appendChild = TreeOutline._appendChild;
414TreeOutline.prototype.insertChild = TreeOutline._insertChild;
415TreeOutline.prototype.removeChild = TreeOutline._removeChild;
416TreeOutline.prototype.removeChildAtIndex = TreeOutline._removeChildAtIndex;
417TreeOutline.prototype.removeChildren = TreeOutline._removeChildren;
418TreeOutline.prototype.removeChildrenRecursive = TreeOutline._removeChildrenRecursive;
419
420function TreeElement(title, representedObject, hasChildren)
421{
422    this._title = title;
423    this.representedObject = (representedObject || {});
424
425    if (this.representedObject.__treeElementIdentifier)
426        this.identifier = this.representedObject.__treeElementIdentifier;
427    else {
428        this.identifier = TreeOutline._knownTreeElementNextIdentifier++;
429        this.representedObject.__treeElementIdentifier = this.identifier;
430    }
431
432    this._hidden = false;
433    this.expanded = false;
434    this.selected = false;
435    this.hasChildren = hasChildren;
436    this.children = [];
437    this.treeOutline = null;
438    this.parent = null;
439    this.previousSibling = null;
440    this.nextSibling = null;
441    this._listItemNode = null;
442}
443
444TreeElement.prototype = {
445    selectable: true,
446    arrowToggleWidth: 10,
447
448    get listItemElement() {
449        return this._listItemNode;
450    },
451
452    get childrenListElement() {
453        return this._childrenListNode;
454    },
455
456    get title() {
457        return this._title;
458    },
459
460    set title(x) {
461        this._title = x;
462        if (this._listItemNode)
463            this._listItemNode.innerHTML = x;
464    },
465
466    get tooltip() {
467        return this._tooltip;
468    },
469
470    set tooltip(x) {
471        this._tooltip = x;
472        if (this._listItemNode)
473            this._listItemNode.title = x ? x : "";
474    },
475
476    get hasChildren() {
477        return this._hasChildren;
478    },
479
480    set hasChildren(x) {
481        if (this._hasChildren === x)
482            return;
483
484        this._hasChildren = x;
485
486        if (!this._listItemNode)
487            return;
488
489        if (x)
490            this._listItemNode.addStyleClass("parent");
491        else {
492            this._listItemNode.removeStyleClass("parent");
493            this.collapse();
494        }
495    },
496
497    get hidden() {
498        return this._hidden;
499    },
500
501    set hidden(x) {
502        if (this._hidden === x)
503            return;
504
505        this._hidden = x;
506
507        if (x) {
508            if (this._listItemNode)
509                this._listItemNode.addStyleClass("hidden");
510            if (this._childrenListNode)
511                this._childrenListNode.addStyleClass("hidden");
512        } else {
513            if (this._listItemNode)
514                this._listItemNode.removeStyleClass("hidden");
515            if (this._childrenListNode)
516                this._childrenListNode.removeStyleClass("hidden");
517        }
518    },
519
520    get shouldRefreshChildren() {
521        return this._shouldRefreshChildren;
522    },
523
524    set shouldRefreshChildren(x) {
525        this._shouldRefreshChildren = x;
526        if (x && this.expanded)
527            this.expand();
528    }
529}
530
531TreeElement.prototype.appendChild = TreeOutline._appendChild;
532TreeElement.prototype.insertChild = TreeOutline._insertChild;
533TreeElement.prototype.removeChild = TreeOutline._removeChild;
534TreeElement.prototype.removeChildAtIndex = TreeOutline._removeChildAtIndex;
535TreeElement.prototype.removeChildren = TreeOutline._removeChildren;
536TreeElement.prototype.removeChildrenRecursive = TreeOutline._removeChildrenRecursive;
537
538TreeElement.prototype._attach = function()
539{
540    if (!this._listItemNode || this.parent._shouldRefreshChildren) {
541        if (this._listItemNode && this._listItemNode.parentNode)
542            this._listItemNode.parentNode.removeChild(this._listItemNode);
543
544        this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
545        this._listItemNode.treeElement = this;
546        this._listItemNode.innerHTML = this._title;
547        this._listItemNode.title = this._tooltip ? this._tooltip : "";
548
549        if (this.hidden)
550            this._listItemNode.addStyleClass("hidden");
551        if (this.hasChildren)
552            this._listItemNode.addStyleClass("parent");
553        if (this.expanded)
554            this._listItemNode.addStyleClass("expanded");
555        if (this.selected)
556            this._listItemNode.addStyleClass("selected");
557
558        this._listItemNode.addEventListener("mousedown", TreeElement.treeElementSelected, false);
559        this._listItemNode.addEventListener("click", TreeElement.treeElementToggled, false);
560        this._listItemNode.addEventListener("dblclick", TreeElement.treeElementDoubleClicked, false);
561
562        if (this.onattach)
563            this.onattach(this);
564    }
565
566    var nextSibling = null;
567    if (this.nextSibling && this.nextSibling._listItemNode && this.nextSibling._listItemNode.parentNode === this.parent._childrenListNode)
568        nextSibling = this.nextSibling._listItemNode;
569    this.parent._childrenListNode.insertBefore(this._listItemNode, nextSibling);
570    if (this._childrenListNode)
571        this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
572    if (this.selected)
573        this.select();
574    if (this.expanded)
575        this.expand();
576}
577
578TreeElement.prototype._detach = function()
579{
580    if (this._listItemNode && this._listItemNode.parentNode)
581        this._listItemNode.parentNode.removeChild(this._listItemNode);
582    if (this._childrenListNode && this._childrenListNode.parentNode)
583        this._childrenListNode.parentNode.removeChild(this._childrenListNode);
584}
585
586TreeElement.treeElementSelected = function(event)
587{
588    var element = event.currentTarget;
589    if (!element || !element.treeElement || !element.treeElement.selectable)
590        return;
591
592    if (element.treeElement.isEventWithinDisclosureTriangle(event))
593        return;
594
595    element.treeElement.select();
596}
597
598TreeElement.treeElementToggled = function(event)
599{
600    var element = event.currentTarget;
601    if (!element || !element.treeElement)
602        return;
603
604    if (!element.treeElement.isEventWithinDisclosureTriangle(event))
605        return;
606
607    if (element.treeElement.expanded) {
608        if (event.altKey)
609            element.treeElement.collapseRecursively();
610        else
611            element.treeElement.collapse();
612    } else {
613        if (event.altKey)
614            element.treeElement.expandRecursively();
615        else
616            element.treeElement.expand();
617    }
618}
619
620TreeElement.treeElementDoubleClicked = function(event)
621{
622    var element = event.currentTarget;
623    if (!element || !element.treeElement)
624        return;
625
626    if (element.treeElement.ondblclick)
627        element.treeElement.ondblclick.call(element.treeElement, event);
628    else if (element.treeElement.hasChildren && !element.treeElement.expanded)
629        element.treeElement.expand();
630}
631
632TreeElement.prototype.collapse = function()
633{
634    if (this._listItemNode)
635        this._listItemNode.removeStyleClass("expanded");
636    if (this._childrenListNode)
637        this._childrenListNode.removeStyleClass("expanded");
638
639    this.expanded = false;
640    if (this.treeOutline)
641        this.treeOutline._treeElementsExpandedState[this.identifier] = true;
642
643    if (this.oncollapse)
644        this.oncollapse(this);
645}
646
647TreeElement.prototype.collapseRecursively = function()
648{
649    var item = this;
650    while (item) {
651        if (item.expanded)
652            item.collapse();
653        item = item.traverseNextTreeElement(false, this, true);
654    }
655}
656
657TreeElement.prototype.expand = function()
658{
659    if (!this.hasChildren || (this.expanded && !this._shouldRefreshChildren && this._childrenListNode))
660        return;
661
662    if (this.treeOutline && (!this._childrenListNode || this._shouldRefreshChildren)) {
663        if (this._childrenListNode && this._childrenListNode.parentNode)
664            this._childrenListNode.parentNode.removeChild(this._childrenListNode);
665
666        this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
667        this._childrenListNode.parentTreeElement = this;
668        this._childrenListNode.addStyleClass("children");
669
670        if (this.hidden)
671            this._childrenListNode.addStyleClass("hidden");
672
673        if (this.onpopulate)
674            this.onpopulate(this);
675
676        for (var i = 0; i < this.children.length; ++i)
677            this.children[i]._attach();
678
679        delete this._shouldRefreshChildren;
680    }
681
682    if (this._listItemNode) {
683        this._listItemNode.addStyleClass("expanded");
684        if (this._childrenListNode && this._childrenListNode.parentNode != this._listItemNode.parentNode)
685            this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
686    }
687
688    if (this._childrenListNode)
689        this._childrenListNode.addStyleClass("expanded");
690
691    this.expanded = true;
692    if (this.treeOutline)
693        this.treeOutline._treeElementsExpandedState[this.identifier] = true;
694
695    if (this.onexpand)
696        this.onexpand(this);
697}
698
699TreeElement.prototype.expandRecursively = function(maxDepth)
700{
701    var item = this;
702    var info = {};
703    var depth = 0;
704
705    // The Inspector uses TreeOutlines to represents object properties, so recursive expansion
706    // in some case can be infinite, since JavaScript objects can hold circular references.
707    // So default to a recursion cap of 3 levels, since that gives fairly good results.
708    if (typeof maxDepth === "undefined" || typeof maxDepth === "null")
709        maxDepth = 3;
710
711    while (item) {
712        if (depth < maxDepth)
713            item.expand();
714        item = item.traverseNextTreeElement(false, this, (depth >= maxDepth), info);
715        depth += info.depthChange;
716    }
717}
718
719TreeElement.prototype.hasAncestor = function(ancestor) {
720    if (!ancestor)
721        return false;
722
723    var currentNode = this.parent;
724    while (currentNode) {
725        if (ancestor === currentNode)
726            return true;
727        currentNode = currentNode.parent;
728    }
729
730    return false;
731}
732
733TreeElement.prototype.reveal = function()
734{
735    var currentAncestor = this.parent;
736    while (currentAncestor && !currentAncestor.root) {
737        if (!currentAncestor.expanded)
738            currentAncestor.expand();
739        currentAncestor = currentAncestor.parent;
740    }
741
742    if (this.onreveal)
743        this.onreveal(this);
744}
745
746TreeElement.prototype.revealed = function()
747{
748    var currentAncestor = this.parent;
749    while (currentAncestor && !currentAncestor.root) {
750        if (!currentAncestor.expanded)
751            return false;
752        currentAncestor = currentAncestor.parent;
753    }
754
755    return true;
756}
757
758TreeElement.prototype.select = function(supressOnSelect)
759{
760    if (!this.treeOutline || !this.selectable || this.selected)
761        return;
762
763    if (this.treeOutline.selectedTreeElement)
764        this.treeOutline.selectedTreeElement.deselect();
765
766    this.selected = true;
767    this.treeOutline.selectedTreeElement = this;
768    if (this._listItemNode)
769        this._listItemNode.addStyleClass("selected");
770
771    if (this.onselect && !supressOnSelect)
772        this.onselect(this);
773}
774
775TreeElement.prototype.deselect = function(supressOnDeselect)
776{
777    if (!this.treeOutline || this.treeOutline.selectedTreeElement !== this || !this.selected)
778        return;
779
780    this.selected = false;
781    this.treeOutline.selectedTreeElement = null;
782    if (this._listItemNode)
783        this._listItemNode.removeStyleClass("selected");
784
785    if (this.ondeselect && !supressOnDeselect)
786        this.ondeselect(this);
787}
788
789TreeElement.prototype.traverseNextTreeElement = function(skipHidden, stayWithin, dontPopulate, info)
790{
791    if (!dontPopulate && this.hasChildren && this.onpopulate)
792        this.onpopulate(this);
793
794    if (info)
795        info.depthChange = 0;
796
797    var element = skipHidden ? (this.revealed() ? this.children[0] : null) : this.children[0];
798    if (element && (!skipHidden || (skipHidden && this.expanded))) {
799        if (info)
800            info.depthChange = 1;
801        return element;
802    }
803
804    if (this === stayWithin)
805        return null;
806
807    element = skipHidden ? (this.revealed() ? this.nextSibling : null) : this.nextSibling;
808    if (element)
809        return element;
810
811    element = this;
812    while (element && !element.root && !(skipHidden ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) && element.parent !== stayWithin) {
813        if (info)
814            info.depthChange -= 1;
815        element = element.parent;
816    }
817
818    if (!element)
819        return null;
820
821    return (skipHidden ? (element.revealed() ? element.nextSibling : null) : element.nextSibling);
822}
823
824TreeElement.prototype.traversePreviousTreeElement = function(skipHidden, dontPopulate)
825{
826    var element = skipHidden ? (this.revealed() ? this.previousSibling : null) : this.previousSibling;
827    if (!dontPopulate && element && element.hasChildren && element.onpopulate)
828        element.onpopulate(element);
829
830    while (element && (skipHidden ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1])) {
831        if (!dontPopulate && element.hasChildren && element.onpopulate)
832            element.onpopulate(element);
833        element = (skipHidden ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1]);
834    }
835
836    if (element)
837        return element;
838
839    if (!this.parent || this.parent.root)
840        return null;
841
842    return this.parent;
843}
844
845TreeElement.prototype.isEventWithinDisclosureTriangle = function(event)
846{
847    var left = this._listItemNode.totalOffsetLeft;
848    return event.pageX >= left && event.pageX <= left + this.arrowToggleWidth && this.hasChildren;
849}
850