7505affddd294f8afca973a8956c8c33a4093dad
[WebKit-https.git] / WebCore / page / inspector / DocumentPanel.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 WebInspector.DocumentPanel = function(resource, views)
30 {
31     var allViews = [{ title: WebInspector.UIString("DOM"), name: "dom" }];
32     if (views)
33         allViews = allViews.concat(views);
34
35     WebInspector.SourcePanel.call(this, resource, allViews);
36
37     var panel = this;
38     var domView = this.views.dom;
39     domView.hide = function() { InspectorController.hideDOMNodeHighlight() };
40     domView.show = function() {
41         InspectorController.highlightDOMNode(panel.focusedDOMNode);
42         panel.updateBreadcrumb();
43         panel.updateTreeSelection();
44     };
45
46     domView.sideContentElement = document.createElement("div");
47     domView.sideContentElement.className = "content side";
48
49     domView.treeContentElement = document.createElement("div");
50     domView.treeContentElement.className = "content tree outline-disclosure";
51
52     domView.treeListElement = document.createElement("ol");
53     domView.treeOutline = new TreeOutline(domView.treeListElement);
54     domView.treeOutline.panel = this;
55
56     domView.crumbsElement = document.createElement("div");
57     domView.crumbsElement.className = "crumbs";
58
59     domView.innerCrumbsElement = document.createElement("div");
60     domView.crumbsElement.appendChild(domView.innerCrumbsElement);
61
62     domView.sidebarPanes = {};
63     domView.sidebarPanes.styles = new WebInspector.StylesSidebarPane();
64     domView.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
65     domView.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
66
67     domView.sidebarPanes.styles.onexpand = function() { panel.updateStyles() };
68     domView.sidebarPanes.metrics.onexpand = function() { panel.updateMetrics() };
69     domView.sidebarPanes.properties.onexpand = function() { panel.updateProperties() };
70
71     domView.sidebarPanes.styles.expanded = true;
72
73     domView.sidebarElement = document.createElement("div");
74     domView.sidebarElement.className = "sidebar";
75
76     domView.sidebarElement.appendChild(domView.sidebarPanes.styles.element);
77     domView.sidebarElement.appendChild(domView.sidebarPanes.metrics.element);
78     domView.sidebarElement.appendChild(domView.sidebarPanes.properties.element);
79
80     domView.sideContentElement.appendChild(domView.treeContentElement);
81     domView.sideContentElement.appendChild(domView.crumbsElement);
82     domView.treeContentElement.appendChild(domView.treeListElement);
83
84     domView.sidebarResizeElement = document.createElement("div");
85     domView.sidebarResizeElement.className = "sidebar-resizer-vertical sidebar-resizer-vertical-right";
86     domView.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false);
87
88     domView.contentElement.appendChild(domView.sideContentElement);
89     domView.contentElement.appendChild(domView.sidebarElement);
90     domView.contentElement.appendChild(domView.sidebarResizeElement);
91
92     this.rootDOMNode = this.resource.documentNode;
93 }
94
95 WebInspector.DocumentPanel.prototype = {
96     resize: function()
97     {
98         this.updateTreeSelection();
99         this.updateBreadcrumbSizes();
100     },
101
102     updateTreeSelection: function()
103     {
104         if (!this.views.dom.treeOutline || !this.views.dom.treeOutline.selectedTreeElement)
105             return;
106         var element = this.views.dom.treeOutline.selectedTreeElement;
107         element.updateSelection();
108     },
109
110     get rootDOMNode()
111     {
112         return this._rootDOMNode;
113     },
114
115     set rootDOMNode(x)
116     {
117         if (this._rootDOMNode === x)
118             return;
119
120         this._rootDOMNode = x;
121
122         this.updateBreadcrumb();
123         this.updateTreeOutline();
124     },
125
126     get focusedDOMNode()
127     {
128         return this._focusedDOMNode;
129     },
130
131     set focusedDOMNode(x)
132     {
133         if (this._focusedDOMNode === x) {
134             var nodeItem = this.revealNode(x);
135             if (nodeItem)
136                 nodeItem.select();
137             return;
138         }
139
140         this._focusedDOMNode = x;
141
142         this.updateBreadcrumb();
143
144         for (var pane in this.views.dom.sidebarPanes)
145             this.views.dom.sidebarPanes[pane].needsUpdate = true;
146
147         this.updateStyles();
148         this.updateMetrics();
149         this.updateProperties();
150
151         InspectorController.highlightDOMNode(x);
152
153         var nodeItem = this.revealNode(x);
154         if (nodeItem)
155             nodeItem.select();
156     },
157
158     revealNode: function(node)
159     {
160         var nodeItem = this.views.dom.treeOutline.findTreeElement(node, function(a, b) { return isAncestorNode.call(a, b); }, function(a) { return a.parentNode; });
161         if (!nodeItem)
162             return;
163
164         nodeItem.reveal();
165         return nodeItem;
166     },
167
168     updateTreeOutline: function()
169     {
170         this.views.dom.treeOutline.removeChildrenRecursive();
171
172         if (!this.rootDOMNode)
173             return;
174
175         // FIXME: this could use findTreeElement to reuse a tree element if it already exists
176         var node = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.rootDOMNode) : this.rootDOMNode.firstChild);
177         while (node) {
178             this.views.dom.treeOutline.appendChild(new WebInspector.DOMNodeTreeElement(node));
179             node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
180         }
181
182         this.updateTreeSelection();
183     },
184
185     updateBreadcrumb: function()
186     {
187         if (!this.visible)
188             return;
189
190         var crumbs = this.views.dom.innerCrumbsElement;
191
192         var handled = false;
193         var foundRoot = false;
194         var crumb = crumbs.firstChild;
195         while (crumb) {
196             if (crumb.representedObject === this.rootDOMNode)
197                 foundRoot = true;
198
199             if (foundRoot)
200                 crumb.addStyleClass("dimmed");
201             else
202                 crumb.removeStyleClass("dimmed");
203
204             if (crumb.representedObject === this.focusedDOMNode) {
205                 crumb.addStyleClass("selected");
206                 handled = true;
207             } else {
208                 crumb.removeStyleClass("selected");
209             }
210
211             crumb = crumb.nextSibling;
212         }
213
214         if (handled) {
215             // We don't need to rebuild the crumbs, but we need to adjust sizes
216             // to reflect the new focused or root node.
217             this.updateBreadcrumbSizes();
218             return;
219         }
220
221         crumbs.removeChildren();
222
223         var panel = this;
224         var selectCrumbFunction = function(event) {
225             var crumb = event.currentTarget;
226             if (crumb.hasStyleClass("collapsed")) {
227                 // Clicking a collapsed crumb will expose the hidden crumbs.
228                 if (crumb === panel.views.dom.innerCrumbsElement.firstChild) {
229                     // If the focused crumb is the first child, pick the farthest crumb
230                     // that is still hidden. This allows the user to expose every crumb.
231                     var currentCrumb = crumb;
232                     while (currentCrumb) {
233                         var hidden = currentCrumb.hasStyleClass("hidden");
234                         var collapsed = currentCrumb.hasStyleClass("collapsed");
235                         if (!hidden && !collapsed)
236                             break;
237                         crumb = currentCrumb;
238                         currentCrumb = currentCrumb.nextSibling;
239                     }
240                 }
241
242                 panel.updateBreadcrumbSizes(crumb);
243             } else {
244                 // Clicking a dimmed crumb or double clicking (event.detail >= 2)
245                 // will change the root node in addition to the focused node.
246                 if (event.detail >= 2 || crumb.hasStyleClass("dimmed"))
247                     panel.rootDOMNode = crumb.representedObject.parentNode;
248                 panel.focusedDOMNode = crumb.representedObject;
249             }
250
251             event.preventDefault();
252         };
253
254         var mouseOverCrumbFunction = function(event) {
255             panel.mouseOverCrumb = true;
256
257             if ("mouseOutTimeout" in panel) {
258                 clearTimeout(panel.mouseOutTimeout);
259                 delete panel.mouseOutTimeout;
260             }
261         };
262
263         var mouseOutCrumbFunction = function(event) {
264             delete panel.mouseOverCrumb;
265
266             if ("mouseOutTimeout" in panel) {
267                 clearTimeout(panel.mouseOutTimeout);
268                 delete panel.mouseOutTimeout;
269             }
270
271             var timeoutFunction = function() {
272                 if (!panel.mouseOverCrumb)
273                     panel.updateBreadcrumbSizes();
274             };
275
276             panel.mouseOutTimeout = setTimeout(timeoutFunction, 500);
277         };
278
279         foundRoot = false;
280         var current = this.focusedDOMNode;
281         while (current) {
282             if (current.nodeType === Node.DOCUMENT_NODE)
283                 break;
284
285             if (current === this.rootDOMNode)
286                 foundRoot = true;
287
288             var crumb = document.createElement("span");
289             crumb.className = "crumb";
290             crumb.representedObject = current;
291             crumb.addEventListener("mousedown", selectCrumbFunction, false);
292             crumb.addEventListener("mouseover", mouseOverCrumbFunction, false);
293             crumb.addEventListener("mouseout", mouseOutCrumbFunction, false);
294
295             var crumbTitle;
296             switch (current.nodeType) {
297                 case Node.ELEMENT_NODE:
298                     crumbTitle = current.nodeName.toLowerCase();
299
300                     var nameElement = document.createElement("span");
301                     nameElement.textContent = crumbTitle;
302                     crumb.appendChild(nameElement);
303
304                     var idAttribute = current.getAttribute("id");
305                     if (idAttribute) {
306                         var idElement = document.createElement("span");
307                         crumb.appendChild(idElement);
308
309                         var part = "#" + idAttribute;
310                         crumbTitle += part;
311                         idElement.appendChild(document.createTextNode(part));
312
313                         // Mark the name as extra, since the ID is more important.
314                         nameElement.className = "extra";
315                     }
316
317                     var classAttribute = current.getAttribute("class");
318                     if (classAttribute) {
319                         var classes = classAttribute.split(/\s+/);
320                         var foundClasses = {};
321
322                         if (classes.length) {
323                             var classesElement = document.createElement("span");
324                             classesElement.className = "extra";
325                             crumb.appendChild(classesElement);
326
327                             for (var i = 0; i < classes.length; ++i) {
328                                 var className = classes[i];
329                                 if (className && !(className in foundClasses)) {
330                                     var part = "." + className;
331                                     crumbTitle += part;
332                                     classesElement.appendChild(document.createTextNode(part));
333                                     foundClasses[className] = true;
334                                 }
335                             }
336                         }
337                     }
338
339                     break;
340
341                 case Node.TEXT_NODE:
342                     if (isNodeWhitespace.call(current))
343                         crumbTitle = WebInspector.UIString("(whitespace)");
344                     else
345                         crumbTitle = WebInspector.UIString("(text)");
346                     break
347
348                 case Node.COMMENT_NODE:
349                     crumbTitle = "<!-->";
350                     break;
351
352                 default:
353                     crumbTitle = current.nodeName.toLowerCase();
354             }
355
356             if (!crumb.childNodes.length) {
357                 var nameElement = document.createElement("span");
358                 nameElement.textContent = crumbTitle;
359                 crumb.appendChild(nameElement);
360             }
361
362             crumb.title = crumbTitle;
363
364             if (foundRoot)
365                 crumb.addStyleClass("dimmed");
366             if (current === this.focusedDOMNode)
367                 crumb.addStyleClass("selected");
368             if (!crumbs.childNodes.length)
369                 crumb.addStyleClass("end");
370             if (current.parentNode.nodeType === Node.DOCUMENT_NODE)
371                 crumb.addStyleClass("start");
372
373             crumbs.appendChild(crumb);
374             current = current.parentNode;
375         }
376
377         this.updateBreadcrumbSizes();
378     },
379
380     updateBreadcrumbSizes: function(focusedCrumb)
381     {
382         if (!this.visible)
383             return;
384
385         if (document.body.offsetWidth <= 0) {
386             // The stylesheet hasn't loaded yet, so we need to update later.
387             setTimeout(this.updateBreadcrumbSizes.bind(this), 0);
388             return;
389         }
390
391         var crumbs = this.views.dom.innerCrumbsElement;
392         if (!crumbs.childNodes.length)
393             return; // No crumbs, do nothing.
394
395         var crumbsContainer = this.views.dom.crumbsElement;
396         if (crumbsContainer.offsetWidth <= 0 || crumbs.offsetWidth <= 0)
397             return;
398
399         // A Zero index is the right most child crumb in the breadcrumb.
400         var selectedIndex = 0;
401         var focusedIndex = 0;
402         var selectedCrumb;
403
404         var i = 0;
405         var crumb = crumbs.firstChild;
406         while (crumb) {
407             // Find the selected crumb and index. 
408             if (!selectedCrumb && crumb.hasStyleClass("selected")) {
409                 selectedCrumb = crumb;
410                 selectedIndex = i;
411             }
412
413             // Find the focused crumb index. 
414             if (crumb === focusedCrumb)
415                 focusedIndex = i;
416
417             // Remove any styles that affect size before
418             // deciding to shorten any crumbs.
419             if (crumb !== crumbs.lastChild)
420                 crumb.removeStyleClass("start");
421             if (crumb !== crumbs.firstChild)
422                 crumb.removeStyleClass("end");
423
424             crumb.removeStyleClass("compact");
425             crumb.removeStyleClass("collapsed");
426             crumb.removeStyleClass("hidden");
427
428             crumb = crumb.nextSibling;
429             ++i;
430         }
431
432         // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs().
433         // The order of the crumbs in the document is opposite of the visual order.
434         crumbs.firstChild.addStyleClass("end");
435         crumbs.lastChild.addStyleClass("start");
436
437         function crumbsAreSmallerThanContainer()
438         {
439             // There is some fixed extra space that is not returned in the crumbs' offsetWidth.
440             // This padding is added to the crumbs' offsetWidth when comparing to the crumbsContainer.
441             var rightPadding = 9;
442             return ((crumbs.offsetWidth + rightPadding) < crumbsContainer.offsetWidth);
443         }
444
445         if (crumbsAreSmallerThanContainer())
446             return; // No need to compact the crumbs, they all fit at full size.
447
448         var BothSides = 0;
449         var AncestorSide = -1;
450         var ChildSide = 1;
451
452         function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb)
453         {
454             if (!significantCrumb)
455                 significantCrumb = (focusedCrumb || selectedCrumb);
456
457             if (significantCrumb === selectedCrumb)
458                 var significantIndex = selectedIndex;
459             else if (significantCrumb === focusedCrumb)
460                 var significantIndex = focusedIndex;
461             else {
462                 var significantIndex = 0;
463                 for (var i = 0; i < crumbs.childNodes.length; ++i) {
464                     if (crumbs.childNodes[i] === significantCrumb) {
465                         significantIndex = i;
466                         break;
467                     }
468                 }
469             }
470
471             function shrinkCrumbAtIndex(index)
472             {
473                 var shrinkCrumb = crumbs.childNodes[index];
474                 if (shrinkCrumb && shrinkCrumb !== significantCrumb)
475                     shrinkingFunction(shrinkCrumb);
476                 if (crumbsAreSmallerThanContainer())
477                     return true; // No need to compact the crumbs more.
478                 return false;
479             }
480
481             // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
482             // fit in the crumbsContainer or we run out of crumbs to shrink.
483             if (direction) {
484                 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
485                 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
486                 while (index !== significantIndex) {
487                     if (shrinkCrumbAtIndex(index))
488                         return true;
489                     index += (direction > 0 ? 1 : -1);
490                 }
491             } else {
492                 // Crumbs are shrunk in order of descending distance from the signifcant crumb,
493                 // with a tie going to child crumbs.
494                 var startIndex = 0;
495                 var endIndex = crumbs.childNodes.length - 1;
496                 while (startIndex != significantIndex || endIndex != significantIndex) {
497                     var startDistance = significantIndex - startIndex;
498                     var endDistance = endIndex - significantIndex;
499                     if (startDistance >= endDistance)
500                         var index = startIndex++;
501                     else
502                         var index = endIndex--;
503                     if (shrinkCrumbAtIndex(index))
504                         return true;
505                 }
506             }
507
508             // We are not small enough yet, return false so the caller knows.
509             return false;
510         }
511
512         function coalesceCollapsedCrumbs()
513         {
514             var crumb = crumbs.firstChild;
515             var collapsedRun = false;
516             var newStartNeeded = false;
517             var newEndNeeded = false;
518             while (crumb) {
519                 var hidden = crumb.hasStyleClass("hidden");
520                 if (!hidden) {
521                     var collapsed = crumb.hasStyleClass("collapsed"); 
522                     if (collapsedRun && collapsed) {
523                         crumb.addStyleClass("hidden");
524                         crumb.removeStyleClass("compact");
525                         crumb.removeStyleClass("collapsed");
526
527                         if (crumb.hasStyleClass("start")) {
528                             crumb.removeStyleClass("start");
529                             newStartNeeded = true;
530                         }
531
532                         if (crumb.hasStyleClass("end")) {
533                             crumb.removeStyleClass("end");
534                             newEndNeeded = true;
535                         }
536
537                         continue;
538                     }
539
540                     collapsedRun = collapsed;
541
542                     if (newEndNeeded) {
543                         newEndNeeded = false;
544                         crumb.addStyleClass("end");
545                     }
546                 } else
547                     collapsedRun = true;
548                 crumb = crumb.nextSibling;
549             }
550
551             if (newStartNeeded) {
552                 crumb = crumbs.lastChild;
553                 while (crumb) {
554                     if (!crumb.hasStyleClass("hidden")) {
555                         crumb.addStyleClass("start");
556                         break;
557                     }
558                     crumb = crumb.previousSibling;
559                 }
560             }
561         }
562
563         function compact(crumb)
564         {
565             if (crumb.hasStyleClass("hidden"))
566                 return;
567             crumb.addStyleClass("compact");
568         }
569
570         function collapse(crumb, dontCoalesce)
571         {
572             if (crumb.hasStyleClass("hidden"))
573                 return;
574             crumb.addStyleClass("collapsed");
575             crumb.removeStyleClass("compact");
576             if (!dontCoalesce)
577                 coalesceCollapsedCrumbs();
578         }
579
580         function compactDimmed(crumb)
581         {
582             if (crumb.hasStyleClass("dimmed"))
583                 compact(crumb);
584         }
585
586         function collapseDimmed(crumb)
587         {
588             if (crumb.hasStyleClass("dimmed"))
589                 collapse(crumb);
590         }
591
592         if (!focusedCrumb) {
593             // When not focused on a crumb we can be biased and collapse less important
594             // crumbs that the user might not care much about.
595
596             // Compact child crumbs.
597             if (makeCrumbsSmaller(compact, ChildSide))
598                 return;
599
600             // Collapse child crumbs.
601             if (makeCrumbsSmaller(collapse, ChildSide))
602                 return;
603
604             // Compact dimmed ancestor crumbs.
605             if (makeCrumbsSmaller(compactDimmed, AncestorSide))
606                 return;
607
608             // Collapse dimmed ancestor crumbs.
609             if (makeCrumbsSmaller(collapseDimmed, AncestorSide))
610                 return;
611         }
612
613         // Compact ancestor crumbs, or from both sides if focused.
614         if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide)))
615             return;
616
617         // Collapse ancestor crumbs, or from both sides if focused.
618         if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide)))
619             return;
620
621         if (!selectedCrumb)
622             return;
623
624         // Compact the selected crumb.
625         compact(selectedCrumb);
626         if (crumbsAreSmallerThanContainer())
627             return;
628
629         // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
630         collapse(selectedCrumb, true);
631     },
632
633     updateStyles: function()
634     {
635         var stylesSidebarPane = this.views.dom.sidebarPanes.styles;
636         if (!stylesSidebarPane.expanded || !stylesSidebarPane.needsUpdate)
637             return;
638
639         stylesSidebarPane.update(this.focusedDOMNode);
640         stylesSidebarPane.needsUpdate = false;
641     },
642
643     updateMetrics: function()
644     {
645         var metricsSidebarPane = this.views.dom.sidebarPanes.metrics;
646         if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate)
647             return;
648
649         metricsSidebarPane.update(this.focusedDOMNode);
650         metricsSidebarPane.needsUpdate = false;
651     },
652
653     updateProperties: function()
654     {
655         var propertiesSidebarPane = this.views.dom.sidebarPanes.properties;
656         if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate)
657             return;
658
659         propertiesSidebarPane.update(this.focusedDOMNode);
660         propertiesSidebarPane.needsUpdate = false;
661     },
662
663     handleKeyEvent: function(event)
664     {
665         if (this.views.dom.treeOutline && this.currentView && this.currentView === this.views.dom)
666             this.views.dom.treeOutline.handleKeyEvent(event);
667     },
668
669     handleCopyEvent: function(event)
670     {
671         if (this.currentView !== this.views.dom)
672             return;
673
674         // Don't prevent the normal copy if the user has a selection.
675         if (!window.getSelection().isCollapsed)
676             return;
677
678         switch (this.focusedDOMNode.nodeType) {
679             case Node.ELEMENT_NODE:
680                 var data = this.focusedDOMNode.outerHTML;
681                 break;
682
683             case Node.COMMENT_NODE:
684                 var data = "<!--" + this.focusedDOMNode.nodeValue + "-->";
685                 break;
686
687             default:
688             case Node.TEXT_NODE:
689                 var data = this.focusedDOMNode.nodeValue;
690         }
691
692         event.clipboardData.clearData();
693         event.preventDefault();
694
695         if (data)
696             event.clipboardData.setData("text/plain", data);
697     },
698
699     rightSidebarResizerDragStart: function(event)
700     {
701         var panel = this; 
702         WebInspector.dividerDragStart(this.views.dom.sidebarElement, function(event) { panel.rightSidebarResizerDrag(event) }, function(event) { panel.rightSidebarResizerDragEnd(event) }, event, "col-resize");
703     },
704
705     rightSidebarResizerDragEnd: function(event)
706     {
707         var panel = this;
708         WebInspector.dividerDragEnd(this.views.dom.sidebarElement, function(event) { panel.rightSidebarResizerDrag(event) }, function(event) { panel.rightSidebarResizerDragEnd(event) }, event);
709     },
710
711     rightSidebarResizerDrag: function(event)
712     {
713         var rightSidebar = this.views.dom.sidebarElement;
714         if (rightSidebar.dragging == true) {
715             var x = event.clientX + window.scrollX;
716
717             var leftSidebarWidth = window.getComputedStyle(document.getElementById("sidebar")).getPropertyCSSValue("width").getFloatValue(CSSPrimitiveValue.CSS_PX);
718             var newWidth = Number.constrain(window.innerWidth - x, 100, window.innerWidth - leftSidebarWidth - 100);
719
720             if (x == newWidth)
721                 rightSidebar.dragLastX = x;
722
723             rightSidebar.style.width = newWidth + "px";
724             this.views.dom.sideContentElement.style.right = newWidth + "px";
725             this.views.dom.sidebarResizeElement.style.right = (newWidth - 3) + "px";
726
727             this.updateTreeSelection();
728             this.updateBreadcrumbSizes();
729
730             event.preventDefault();
731         }
732     }
733 }
734
735 WebInspector.DocumentPanel.prototype.__proto__ = WebInspector.SourcePanel.prototype;
736
737 WebInspector.DOMNodeTreeElement = function(node)
738 {
739     var hasChildren = (Preferences.ignoreWhitespace ? (firstChildSkippingWhitespace.call(node) ? true : false) : node.hasChildNodes());
740     var titleInfo = nodeTitleInfo.call(node, hasChildren, WebInspector.linkifyURL);
741
742     if (titleInfo.hasChildren) 
743         this.whitespaceIgnored = Preferences.ignoreWhitespace;
744
745     TreeElement.call(this, titleInfo.title, node, titleInfo.hasChildren);
746 }
747
748 WebInspector.DOMNodeTreeElement.prototype = {
749     updateSelection: function()
750     {
751         var listItemElement = this.listItemElement;
752         if (!listItemElement)
753             return;
754
755         if (document.body.offsetWidth <= 0) {
756             // The stylesheet hasn't loaded yet, so we need to update later.
757             setTimeout(this.updateSelection.bind(this), 0);
758             return;
759         }
760
761         if (!this.selectionElement) {
762             this.selectionElement = document.createElement("div");
763             this.selectionElement.className = "selection selected";
764             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
765         }
766
767         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
768     },
769
770     onattach: function()
771     {
772         this.listItemElement.addEventListener("mousedown", this.onmousedown.bind(this), false);
773     },
774
775     onpopulate: function()
776     {
777         if (this.children.length || this.whitespaceIgnored !== Preferences.ignoreWhitespace)
778             return;
779
780         this.removeChildren();
781         this.whitespaceIgnored = Preferences.ignoreWhitespace;
782
783         var node = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.representedObject) : this.representedObject.firstChild);
784         while (node) {
785             this.appendChild(new WebInspector.DOMNodeTreeElement(node));
786             node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
787         }
788
789         if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
790             var title = "<span class=\"webkit-html-tag close\">&lt;/" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "&gt;</span>";
791             var item = new TreeElement(title, this.representedObject, false);
792             item.selectable = false;
793             this.appendChild(item);
794         }
795     },
796
797     onexpand: function()
798     {
799         this.treeOutline.panel.updateTreeSelection();
800     },
801
802     oncollapse: function()
803     {
804         this.treeOutline.panel.updateTreeSelection();
805     },
806
807     onreveal: function()
808     {
809         if (!this.listItemElement || !this.treeOutline)
810             return;
811         this.treeOutline.panel.views.dom.treeContentElement.scrollToElement(this.listItemElement);
812     },
813
814     onselect: function()
815     {
816         this.treeOutline.panel.focusedDOMNode = this.representedObject;
817         this.updateSelection();
818     },
819
820     onmousedown: function(event)
821     {
822         // Prevent selecting the nearest word on double click.
823         if (event.detail >= 2)
824             event.preventDefault();
825     },
826
827     ondblclick: function()
828     {
829         var panel = this.treeOutline.panel;
830         panel.rootDOMNode = this.representedObject.parentNode;
831         panel.focusedDOMNode = this.representedObject;
832
833         if (this.hasChildren && !this.expanded)
834             this.expand();
835     }
836 }
837
838 WebInspector.DOMNodeTreeElement.prototype.__proto__ = TreeElement.prototype;