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