Reviewed by Adam.
[WebKit-https.git] / WebCore / page / inspector / treeoutline.js
1 /*
2  * Copyright (C) 2007 Apple Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 function 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
45 TreeOutline._knownTreeElementNextIdentifier = 1;
46
47 TreeOutline._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
88 TreeOutline._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
136 TreeOutline._removeChild = function(child)
137 {
138     if (!child)
139         throw("child can't be undefined or null");
140
141     for (var i = 0; i < this.children.length; ++i) {
142         if (this.children[i] === child) {
143             this.children.splice(i, 1);
144             break;
145         }
146     }
147
148     child.deselect();
149
150     if (child.previousSibling)
151         child.previousSibling.nextSibling = child.nextSibling;
152     if (child.nextSibling)
153         child.nextSibling.previousSibling = child.previousSibling;
154
155     if (child.treeOutline)
156         child.treeOutline._forgetTreeElement(child);
157     child._detach();
158     child.treeOutline = null;
159     child.parent = null;
160     child.nextSibling = null;
161     child.previousSibling = null;
162 }
163
164 TreeOutline._removeChildren = function()
165 {
166     for (var i = 0; i < this.children.length; ++i) {
167         var child = this.children[i];
168         child.deselect();
169         if (child.treeOutline)
170             child.treeOutline._forgetTreeElement(child);
171         child._detach();
172         child.treeOutline = null;
173         child.parent = null;
174         child.nextSibling = null;
175         child.previousSibling = null;
176     }
177
178     this.children = [];
179
180     if (this._childrenListNode)
181         this._childrenListNode.offsetTop; // force layout
182 }
183
184 TreeOutline._removeChildrenRecursive = function()
185 {
186     var childrenToRemove = this.children;
187
188     var child = this.children[0];
189     while (child) {
190         if (child.children.length)
191             childrenToRemove = childrenToRemove.concat(child.children);
192         child = child.traverseNextTreeElement(false, this, true);
193     }
194
195     for (var i = 0; i < childrenToRemove.length; ++i) {
196         var child = childrenToRemove[i];
197         child.deselect();
198         if (child.treeOutline)
199             child.treeOutline._forgetTreeElement(child);
200         child._detach();
201         child.children = [];
202         child.treeOutline = null;
203         child.parent = null;
204         child.nextSibling = null;
205         child.previousSibling = null;
206     }
207
208     this.children = [];
209 }
210
211 TreeOutline.prototype._rememberTreeElement = function(element)
212 {
213     if (!this._knownTreeElements[element.identifier])
214         this._knownTreeElements[element.identifier] = [];
215
216     // check if the element is already known
217     var elements = this._knownTreeElements[element.identifier];
218     for (var i = 0; i < elements.length; ++i)
219         if (elements[i] === element)
220             return;
221
222     // add the element
223     elements.push(element);
224 }
225
226 TreeOutline.prototype._forgetTreeElement = function(element)
227 {
228     if (!this._knownTreeElements[element.identifier])
229         return;
230
231     var elements = this._knownTreeElements[element.identifier];
232     for (var i = 0; i < elements.length; ++i) {
233         if (elements[i] === element) {
234             elements.splice(i, 1);
235             break;
236         }
237     }
238 }
239
240 TreeOutline.prototype.findTreeElement = function(representedObject, isAncestor, getParent)
241 {
242     if (!representedObject)
243         return null;
244
245     if ("__treeElementIdentifier" in representedObject) {
246         var elements = this._knownTreeElements[representedObject.__treeElementIdentifier];
247         if (!elements)
248             return null;
249
250         for (var i = 0; i < elements.length; ++i)
251             if (elements[i].representedObject === representedObject)
252                 return elements[i];
253     }
254
255     if (!isAncestor || !(isAncestor instanceof Function) || !getParent || !(getParent instanceof Function))
256         return null;
257
258     var item;
259     var found = false;
260     for (var i = 0; i < this.children.length; ++i) {
261         item = this.children[i];
262         if (item.representedObject === representedObject || isAncestor(item.representedObject, representedObject)) {
263             found = true;
264             break;
265         }
266     }
267
268     if (!found)
269         return null;
270
271     var ancestors = [];
272     var currentObject = representedObject;
273     while (currentObject) {
274         ancestors.unshift(currentObject);
275         if (currentObject === item.representedObject)
276             break;
277         currentObject = getParent(currentObject);
278     }
279
280     for (var i = 0; i < ancestors.length; ++i) {
281         item = this.findTreeElement(ancestors[i], isAncestor, getParent);
282         if (ancestors[i] !== representedObject && item && item.onpopulate)
283             item.onpopulate(item);
284     }
285
286     return item;
287 }
288
289 TreeOutline.prototype.handleKeyEvent = function(event)
290 {
291     if (!this.selectedTreeElement || event.shiftKey || event.metaKey || event.ctrlKey)
292         return false;
293
294     var handled = false;
295     var nextSelectedElement;
296     if (event.keyIdentifier === "Up" && !event.altKey) {
297         nextSelectedElement = this.selectedTreeElement.traversePreviousTreeElement(true);
298         while (nextSelectedElement && !nextSelectedElement.selectable)
299             nextSelectedElement = nextSelectedElement.traversePreviousTreeElement(!this.expandTreeElementsWhenArrowing);
300         handled = nextSelectedElement ? true : false;
301     } else if (event.keyIdentifier === "Down" && !event.altKey) {
302         nextSelectedElement = this.selectedTreeElement.traverseNextTreeElement(true);
303         while (nextSelectedElement && !nextSelectedElement.selectable)
304             nextSelectedElement = nextSelectedElement.traverseNextTreeElement(!this.expandTreeElementsWhenArrowing);
305         handled = nextSelectedElement ? true : false;
306     } else if (event.keyIdentifier === "Left") {
307         if (this.selectedTreeElement.expanded) {
308             if (event.altKey)
309                 this.selectedTreeElement.collapseRecursively();
310             else
311                 this.selectedTreeElement.collapse();
312             handled = true;
313         } else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) {
314             handled = true;
315             if (this.selectedTreeElement.parent.selectable) {
316                 nextSelectedElement = this.selectedTreeElement.parent;
317                 handled = nextSelectedElement ? true : false;
318             } else if (this.selectedTreeElement.parent)
319                 this.selectedTreeElement.parent.collapse();
320         }
321     } else if (event.keyIdentifier === "Right") {
322         if (!this.selectedTreeElement.revealed()) {
323             this.selectedTreeElement.reveal();
324             handled = true;
325         } else if (this.selectedTreeElement.hasChildren) {
326             handled = true;
327             if (this.selectedTreeElement.expanded) {
328                 nextSelectedElement = this.selectedTreeElement.children[0];
329                 handled = nextSelectedElement ? true : false;
330             } else {
331                 if (event.altKey)
332                     this.selectedTreeElement.expandRecursively();
333                 else
334                     this.selectedTreeElement.expand();
335             }
336         }
337     }
338
339     if (nextSelectedElement) {
340         nextSelectedElement.reveal();
341         nextSelectedElement.select();
342     }
343
344     if (handled) {
345         event.preventDefault();
346         event.stopPropagation();
347     }
348
349     return handled;
350 }
351
352 TreeOutline.prototype.expand = function()
353 {
354     // this is the root, do nothing
355 }
356
357 TreeOutline.prototype.collapse = function()
358 {
359     // this is the root, do nothing
360 }
361
362 TreeOutline.prototype.revealed = function()
363 {
364     return true;
365 }
366
367 TreeOutline.prototype.reveal = function()
368 {
369     // this is the root, do nothing
370 }
371
372 TreeOutline.prototype.appendChild = TreeOutline._appendChild;
373 TreeOutline.prototype.insertChild = TreeOutline._insertChild;
374 TreeOutline.prototype.removeChild = TreeOutline._removeChild;
375 TreeOutline.prototype.removeChildren = TreeOutline._removeChildren;
376 TreeOutline.prototype.removeChildrenRecursive = TreeOutline._removeChildrenRecursive;
377
378 function TreeElement(title, representedObject, hasChildren)
379 {
380     this._title = title;
381     this.representedObject = (representedObject || {});
382
383     if (this.representedObject.__treeElementIdentifier)
384         this.identifier = this.representedObject.__treeElementIdentifier;
385     else {
386         this.identifier = TreeOutline._knownTreeElementNextIdentifier++;
387         this.representedObject.__treeElementIdentifier = this.identifier;
388     }
389
390     this._hidden = false;
391     this.expanded = false;
392     this.selected = false;
393     this.selectable = true;
394     this.hasChildren = hasChildren;
395     this.children = [];
396     this.treeOutline = null;
397     this.parent = null;
398     this.previousSibling = null;
399     this.nextSibling = null;
400     this._listItemNode = null;
401 }
402
403 TreeElement.prototype = {
404     get title() {
405         return this._title;
406     },
407
408     set title(x) {
409         this._title = x;
410         if (this._listItemNode)
411             this._listItemNode.innerHTML = x;
412     },
413
414     get tooltip() {
415         return this._tooltip;
416     },
417
418     set tooltip(x) {
419         this._tooltip = x;
420         if (this._listItemNode)
421             this._listItemNode.title = x ? x : "";
422     },
423
424     get hidden() {
425         return this._hidden;
426     },
427
428     set hidden(x) {
429         if (this._hidden === x)
430             return;
431
432         this._hidden = x;
433
434         if (x) {
435             if (this._listItemNode)
436                 this._listItemNode.addStyleClass("hidden");
437             if (this._childrenListNode)
438                 this._childrenListNode.addStyleClass("hidden");
439         } else {
440             if (this._listItemNode)
441                 this._listItemNode.removeStyleClass("hidden");
442             if (this._childrenListNode)
443                 this._childrenListNode.removeStyleClass("hidden");
444         }
445     }
446 }
447
448 TreeElement.prototype.appendChild = TreeOutline._appendChild;
449 TreeElement.prototype.insertChild = TreeOutline._insertChild;
450 TreeElement.prototype.removeChild = TreeOutline._removeChild;
451 TreeElement.prototype.removeChildren = TreeOutline._removeChildren;
452 TreeElement.prototype.removeChildrenRecursive = TreeOutline._removeChildrenRecursive;
453
454 TreeElement.prototype._attach = function()
455 {
456     if (!this._listItemNode || this.parent.refreshChildren) {
457         if (this._listItemNode && this._listItemNode.parentNode)
458             this._listItemNode.parentNode.removeChild(this._listItemNode);
459
460         this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
461         this._listItemNode.treeElement = this;
462         this._listItemNode.innerHTML = this._title;
463         this._listItemNode.title = this._tooltip ? this._tooltip : "";
464
465         if (this.hidden)
466             this._listItemNode.addStyleClass("hidden");
467         if (this.hasChildren)
468             this._listItemNode.addStyleClass("parent");
469         if (this.expanded)
470             this._listItemNode.addStyleClass("expanded");
471         if (this.selected)
472             this._listItemNode.addStyleClass("selected");
473
474         this._listItemNode.addEventListener("mousedown", TreeElement.treeElementSelected, false);
475         this._listItemNode.addEventListener("click", TreeElement.treeElementToggled, false);
476         this._listItemNode.addEventListener("dblclick", TreeElement.treeElementDoubleClicked, false);
477     }
478
479     this.parent._childrenListNode.insertBefore(this._listItemNode, (this.nextSibling ? this.nextSibling._listItemNode : null));
480     if (this._childrenListNode)
481         this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
482     if (this.selected)
483         this.select();
484     if (this.expanded)
485         this.expand();
486 }
487
488 TreeElement.prototype._detach = function()
489 {
490     if (this._listItemNode && this._listItemNode.parentNode)
491         this._listItemNode.parentNode.removeChild(this._listItemNode);
492     if (this._childrenListNode && this._childrenListNode.parentNode)
493         this._childrenListNode.parentNode.removeChild(this._childrenListNode);
494 }
495
496 TreeElement.treeElementSelected = function(event)
497 {
498     var element = event.currentTarget;
499     if (!element || !element.treeElement || !element.treeElement.selectable)
500         return;
501
502     if (event.offsetX > 20 || !element.treeElement.hasChildren)
503         element.treeElement.select();
504 }
505
506 TreeElement.treeElementToggled = function(event)
507 {
508     var element = event.currentTarget;
509     if (!element || !element.treeElement)
510         return;
511
512     if (event.offsetX <= 20 && element.treeElement.hasChildren) {
513         if (element.treeElement.expanded) {
514             if (event.altKey)
515                 element.treeElement.collapseRecursively();
516             else
517                 element.treeElement.collapse();
518         } else {
519             if (event.altKey)
520                 element.treeElement.expandRecursively();
521             else
522                 element.treeElement.expand();
523         }
524     }
525 }
526
527 TreeElement.treeElementDoubleClicked = function(event)
528 {
529     var element = event.currentTarget;
530     if (!element || !element.treeElement)
531         return;
532
533     if (element.treeElement.hasChildren && !element.treeElement.expanded)
534         element.treeElement.expand();
535
536     if (element.treeElement.ondblclick)
537         element.treeElement.ondblclick(element.treeElement);
538 }
539
540 TreeElement.prototype.collapse = function()
541 {
542     if (this._listItemNode)
543         this._listItemNode.removeStyleClass("expanded");
544     if (this._childrenListNode)
545         this._childrenListNode.removeStyleClass("expanded");
546
547     this.expanded = false;
548     if (this.treeOutline)
549         this.treeOutline._treeElementsExpandedState[this.identifier] = true;
550
551     if (this.oncollapse)
552         this.oncollapse(this);
553 }
554
555 TreeElement.prototype.collapseRecursively = function()
556 {
557     var item = this;
558     while (item) {
559         if (item.expanded)
560             item.collapse();
561         item = item.traverseNextTreeElement(false, this, true);
562     }
563 }
564
565 TreeElement.prototype.expand = function()
566 {
567     if (!this.hasChildren || (this.expanded && !this.refreshChildren && this._childrenListNode))
568         return;
569
570     if (!this._childrenListNode || this.refreshChildren) {
571         if (this._childrenListNode && this._childrenListNode.parentNode)
572             this._childrenListNode.parentNode.removeChild(this._childrenListNode);
573
574         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
575         this._childrenListNode.parentTreeElement = this;
576         this._childrenListNode.addStyleClass("children");
577
578         if (this.hidden)
579             this._childrenListNode.addStyleClass("hidden");
580
581         if (this.onpopulate)
582             this.onpopulate(this);
583
584         for (var i = 0; i < this.children.length; ++i) {
585             var child = this.children[i];
586             child._attach();
587         }
588
589         delete this.refreshChildren;
590     }
591
592     if (this._listItemNode) {
593         this._listItemNode.addStyleClass("expanded");
594         if (this._childrenListNode.parentNode != this._listItemNode.parentNode)
595             this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
596     }
597
598     if (this._childrenListNode)
599         this._childrenListNode.addStyleClass("expanded");
600
601     this.expanded = true;
602     if (this.treeOutline)
603         this.treeOutline._treeElementsExpandedState[this.identifier] = true;
604
605     if (this.onexpand)
606         this.onexpand(this);
607 }
608
609 TreeElement.prototype.expandRecursively = function()
610 {
611     var item = this;
612     while (item) {
613         item.expand();
614         item = item.traverseNextTreeElement(false, this);
615     }
616 }
617
618 TreeElement.prototype.reveal = function()
619 {
620     var currentAncestor = this.parent;
621     while (currentAncestor && !currentAncestor.root) {
622         if (!currentAncestor.expanded)
623             currentAncestor.expand();
624         currentAncestor = currentAncestor.parent;
625     }
626
627     if (this.onreveal)
628         this.onreveal(this);
629 }
630
631 TreeElement.prototype.revealed = function()
632 {
633     var currentAncestor = this.parent;
634     while (currentAncestor && !currentAncestor.root) {
635         if (!currentAncestor.expanded)
636             return false;
637         currentAncestor = currentAncestor.parent;
638     }
639
640     return true;
641 }
642
643 TreeElement.prototype.select = function(supressOnSelect)
644 {
645     if (!this.treeOutline || !this.selectable || this.selected)
646         return;
647
648     if (this.treeOutline.selectedTreeElement)
649         this.treeOutline.selectedTreeElement.deselect();
650
651     this.selected = true;
652     this.treeOutline.selectedTreeElement = this;
653     if (this._listItemNode)
654         this._listItemNode.addStyleClass("selected");
655
656     if (this.onselect && !supressOnSelect)
657         this.onselect(this);
658 }
659
660 TreeElement.prototype.deselect = function(supressOnDeselect)
661 {
662     if (!this.treeOutline || this.treeOutline.selectedTreeElement !== this || !this.selected)
663         return;
664
665     this.selected = false;
666     this.treeOutline.selectedTreeElement = null;
667     if (this._listItemNode)
668         this._listItemNode.removeStyleClass("selected");
669
670     if (this.ondeselect && !supressOnDeselect)
671         this.ondeselect(this);
672 }
673
674 TreeElement.prototype.traverseNextTreeElement = function(skipHidden, stayWithin, dontPopulate)
675 {
676     if (!dontPopulate && this.hasChildren && this.onpopulate)
677         this.onpopulate(this);
678
679     var element = skipHidden ? (this.revealed() ? this.children[0] : null) : this.children[0];
680     if (element && (!skipHidden || (skipHidden && this.expanded)))
681         return element;
682
683     if (this === stayWithin)
684         return null;
685
686     element = skipHidden ? (this.revealed() ? this.nextSibling : null) : this.nextSibling;
687     if (element)
688         return element;
689
690     element = this;
691     while (element && !element.root && !(skipHidden ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) && element.parent !== stayWithin)
692         element = element.parent;
693
694     if (!element)
695         return null;
696
697     return (skipHidden ? (element.revealed() ? element.nextSibling : null) : element.nextSibling);
698 }
699
700 TreeElement.prototype.traversePreviousTreeElement = function(skipHidden, dontPopulate)
701 {
702     var element = skipHidden ? (this.revealed() ? this.previousSibling : null) : this.previousSibling;
703     if (!dontPopulate && element && element.hasChildren && element.onpopulate)
704         element.onpopulate(element);
705
706     while (element && (skipHidden ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1])) {
707         if (!dontPopulate && element.hasChildren && element.onpopulate)
708             element.onpopulate(element);
709         element = (skipHidden ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1]);
710     }
711
712     if (element)
713         return element;
714
715     if (!this.parent || this.parent.root)
716         return null;
717
718     return this.parent;
719 }