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]);
282         if (ancestors[i] !== representedObject && 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.onpopulate = null;
397     this.onexpand = null;
398     this.oncollapse = null;
399     this.onreveal = null;
400     this.onselect = null;
401     this.ondeselect = null;
402     this.ondblclick = null;
403     this.treeOutline = null;
404     this.parent = null;
405     this.previousSibling = null;
406     this.nextSibling = null;
407     this._listItemNode = null;
408 }
409
410 TreeElement.prototype = {
411     get title() {
412         return this._title;
413     },
414
415     set title(x) {
416         this._title = x;
417         if (this._listItemNode)
418             this._listItemNode.innerHTML = x;
419     },
420
421     get tooltip() {
422         return this._tooltip;
423     },
424
425     set tooltip(x) {
426         this._tooltip = x;
427         if (this._listItemNode)
428             this._listItemNode.title = x ? x : "";
429     },
430
431     get hidden() {
432         return this._hidden;
433     },
434
435     set hidden(x) {
436         if (this._hidden === x)
437             return;
438
439         this._hidden = x;
440
441         if (x) {
442             if (this._listItemNode)
443                 this._listItemNode.addStyleClass("hidden");
444             if (this._childrenListNode)
445                 this._childrenListNode.addStyleClass("hidden");
446         } else {
447             if (this._listItemNode)
448                 this._listItemNode.removeStyleClass("hidden");
449             if (this._childrenListNode)
450                 this._childrenListNode.removeStyleClass("hidden");
451         }
452     }
453 }
454
455 TreeElement.prototype.appendChild = TreeOutline._appendChild;
456 TreeElement.prototype.insertChild = TreeOutline._insertChild;
457 TreeElement.prototype.removeChild = TreeOutline._removeChild;
458 TreeElement.prototype.removeChildren = TreeOutline._removeChildren;
459 TreeElement.prototype.removeChildrenRecursive = TreeOutline._removeChildrenRecursive;
460
461 TreeElement.prototype._attach = function()
462 {
463     if (!this._listItemNode || this.parent.refreshChildren) {
464         if (this._listItemNode && this._listItemNode.parentNode)
465             this._listItemNode.parentNode.removeChild(this._listItemNode);
466
467         this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
468         this._listItemNode.treeElement = this;
469         this._listItemNode.innerHTML = this._title;
470         this._listItemNode.title = this._tooltip ? this._tooltip : "";
471
472         if (this.hidden)
473             this._listItemNode.addStyleClass("hidden");
474         if (this.hasChildren)
475             this._listItemNode.addStyleClass("parent");
476         if (this.expanded)
477             this._listItemNode.addStyleClass("expanded");
478         if (this.selected)
479             this._listItemNode.addStyleClass("selected");
480
481         this._listItemNode.addEventListener("mousedown", TreeElement.treeElementSelected, false);
482         this._listItemNode.addEventListener("click", TreeElement.treeElementToggled, false);
483         this._listItemNode.addEventListener("dblclick", TreeElement.treeElementDoubleClicked, false);
484     }
485
486     this.parent._childrenListNode.insertBefore(this._listItemNode, (this.nextSibling ? this.nextSibling._listItemNode : null));
487     if (this._childrenListNode)
488         this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
489     if (this.selected)
490         this.select();
491     if (this.expanded)
492         this.expand();
493 }
494
495 TreeElement.prototype._detach = function()
496 {
497     if (this._listItemNode && this._listItemNode.parentNode)
498         this._listItemNode.parentNode.removeChild(this._listItemNode);
499     if (this._childrenListNode && this._childrenListNode.parentNode)
500         this._childrenListNode.parentNode.removeChild(this._childrenListNode);
501 }
502
503 TreeElement.treeElementSelected = function(event)
504 {
505     var element = event.currentTarget;
506     if (!element || !element.treeElement || !element.treeElement.selectable)
507         return;
508
509     if (event.offsetX > 20 || !element.treeElement.hasChildren)
510         element.treeElement.select();
511 }
512
513 TreeElement.treeElementToggled = function(event)
514 {
515     var element = event.currentTarget;
516     if (!element || !element.treeElement)
517         return;
518
519     if (event.offsetX <= 20 && element.treeElement.hasChildren) {
520         if (element.treeElement.expanded) {
521             if (event.altKey)
522                 element.treeElement.collapseRecursively();
523             else
524                 element.treeElement.collapse();
525         } else {
526             if (event.altKey)
527                 element.treeElement.expandRecursively();
528             else
529                 element.treeElement.expand();
530         }
531     }
532 }
533
534 TreeElement.treeElementDoubleClicked = function(event)
535 {
536     var element = event.currentTarget;
537     if (!element || !element.treeElement)
538         return;
539
540     if (element.treeElement.hasChildren && !element.treeElement.expanded)
541         element.treeElement.expand();
542
543     if (element.treeElement.ondblclick)
544         element.treeElement.ondblclick(element.treeElement);
545 }
546
547 TreeElement.prototype.collapse = function()
548 {
549     if (this._listItemNode)
550         this._listItemNode.removeStyleClass("expanded");
551     if (this._childrenListNode)
552         this._childrenListNode.removeStyleClass("expanded");
553
554     this.expanded = false;
555     if (this.treeOutline)
556         this.treeOutline._treeElementsExpandedState[this.identifier] = true;
557
558     if (this.oncollapse)
559         this.oncollapse(this);
560 }
561
562 TreeElement.prototype.collapseRecursively = function()
563 {
564     var item = this;
565     while (item) {
566         if (item.expanded)
567             item.collapse();
568         item = item.traverseNextTreeElement(false, this, true);
569     }
570 }
571
572 TreeElement.prototype.expand = function()
573 {
574     if (!this.hasChildren || (this.expanded && !this.refreshChildren && this._childrenListNode))
575         return;
576
577     if (!this._childrenListNode || this.refreshChildren) {
578         if (this._childrenListNode && this._childrenListNode.parentNode)
579             this._childrenListNode.parentNode.removeChild(this._childrenListNode);
580
581         this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
582         this._childrenListNode.parentTreeElement = this;
583         this._childrenListNode.addStyleClass("children");
584
585         if (this.hidden)
586             this._childrenListNode.addStyleClass("hidden");
587
588         if (this.onpopulate)
589             this.onpopulate(this);
590
591         for (var i = 0; i < this.children.length; ++i) {
592             var child = this.children[i];
593             child._attach();
594         }
595
596         delete this.refreshChildren;
597     }
598
599     if (this._listItemNode) {
600         this._listItemNode.addStyleClass("expanded");
601         if (this._childrenListNode.parentNode != this._listItemNode.parentNode)
602             this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
603     }
604
605     if (this._childrenListNode)
606         this._childrenListNode.addStyleClass("expanded");
607
608     this.expanded = true;
609     if (this.treeOutline)
610         this.treeOutline._treeElementsExpandedState[this.identifier] = true;
611
612     if (this.onexpand)
613         this.onexpand(this);
614 }
615
616 TreeElement.prototype.expandRecursively = function()
617 {
618     var item = this;
619     while (item) {
620         item.expand();
621         item = item.traverseNextTreeElement(false, this);
622     }
623 }
624
625 TreeElement.prototype.reveal = function()
626 {
627     var currentAncestor = this.parent;
628     while (currentAncestor && !currentAncestor.root) {
629         if (!currentAncestor.expanded)
630             currentAncestor.expand();
631         currentAncestor = currentAncestor.parent;
632     }
633
634     if (this.onreveal)
635         this.onreveal(this);
636 }
637
638 TreeElement.prototype.revealed = function()
639 {
640     var currentAncestor = this.parent;
641     while (currentAncestor && !currentAncestor.root) {
642         if (!currentAncestor.expanded)
643             return false;
644         currentAncestor = currentAncestor.parent;
645     }
646
647     return true;
648 }
649
650 TreeElement.prototype.select = function(supressOnSelect)
651 {
652     if (!this.treeOutline || !this.selectable || this.selected)
653         return;
654
655     if (this.treeOutline.selectedTreeElement)
656         this.treeOutline.selectedTreeElement.deselect();
657
658     this.selected = true;
659     this.treeOutline.selectedTreeElement = this;
660     if (this._listItemNode)
661         this._listItemNode.addStyleClass("selected");
662
663     if (this.onselect && !supressOnSelect)
664         this.onselect(this);
665 }
666
667 TreeElement.prototype.deselect = function(supressOnDeselect)
668 {
669     if (!this.treeOutline || this.treeOutline.selectedTreeElement !== this || !this.selected)
670         return;
671
672     this.selected = false;
673     this.treeOutline.selectedTreeElement = null;
674     if (this._listItemNode)
675         this._listItemNode.removeStyleClass("selected");
676
677     if (this.ondeselect && !supressOnDeselect)
678         this.ondeselect(this);
679 }
680
681 TreeElement.prototype.traverseNextTreeElement = function(skipHidden, stayWithin, dontPopulate)
682 {
683     if (!dontPopulate && this.hasChildren && this.onpopulate)
684         this.onpopulate(this);
685
686     var element = skipHidden ? (this.revealed() ? this.children[0] : null) : this.children[0];
687     if (element && (!skipHidden || (skipHidden && this.expanded)))
688         return element;
689
690     if (this === stayWithin)
691         return null;
692
693     element = skipHidden ? (this.revealed() ? this.nextSibling : null) : this.nextSibling;
694     if (element)
695         return element;
696
697     element = this;
698     while (element && !element.root && !(skipHidden ? (element.revealed() ? element.nextSibling : null) : element.nextSibling) && element.parent !== stayWithin)
699         element = element.parent;
700
701     if (!element)
702         return null;
703
704     return (skipHidden ? (element.revealed() ? element.nextSibling : null) : element.nextSibling);
705 }
706
707 TreeElement.prototype.traversePreviousTreeElement = function(skipHidden, dontPopulate)
708 {
709     var element = skipHidden ? (this.revealed() ? this.previousSibling : null) : this.previousSibling;
710     if (!dontPopulate && element && element.hasChildren && element.onpopulate)
711         element.onpopulate(element);
712
713     while (element && (skipHidden ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1])) {
714         if (!dontPopulate && element.hasChildren && element.onpopulate)
715             element.onpopulate(element);
716         element = (skipHidden ? (element.revealed() && element.expanded ? element.children[element.children.length - 1] : null) : element.children[element.children.length - 1]);
717     }
718
719     if (element)
720         return element;
721
722     if (!this.parent || this.parent.root)
723         return null;
724
725     return this.parent;
726 }