2008-03-26 Steve Falkenburg <sfalken@apple.com>
[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         panel.updateBreadcrumb();
42         panel.updateTreeSelection();
43     };
44
45     domView.sideContentElement = document.createElement("div");
46     domView.sideContentElement.className = "content side";
47
48     domView.treeContentElement = document.createElement("div");
49     domView.treeContentElement.className = "content tree outline-disclosure";
50
51     function clearNodeHighlight(event)
52     {
53         if (event.target === this)
54             InspectorController.hideDOMNodeHighlight();
55     }
56
57     domView.treeListElement = document.createElement("ol");
58     domView.treeListElement.addEventListener("mousedown", this._onmousedown.bind(this), false);
59     domView.treeListElement.addEventListener("dblclick", this._ondblclick.bind(this), false);
60     domView.treeListElement.addEventListener("mousemove", this._onmousemove.bind(this), false);
61     domView.treeListElement.addEventListener("mouseout", clearNodeHighlight.bind(domView.treeListElement), false);
62     domView.treeOutline = new TreeOutline(domView.treeListElement);
63     domView.treeOutline.panel = this;
64
65     domView.crumbsElement = document.createElement("div");
66     domView.crumbsElement.className = "crumbs";
67     domView.crumbsElement.addEventListener("mouseout", clearNodeHighlight.bind(domView.crumbsElement), false);
68
69     domView.innerCrumbsElement = document.createElement("div");
70     domView.crumbsElement.appendChild(domView.innerCrumbsElement);
71
72     domView.sidebarPanes = {};
73     domView.sidebarPanes.styles = new WebInspector.StylesSidebarPane();
74     domView.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
75     domView.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
76
77     domView.sidebarPanes.styles.onexpand = function() { panel.updateStyles() };
78     domView.sidebarPanes.metrics.onexpand = function() { panel.updateMetrics() };
79     domView.sidebarPanes.properties.onexpand = function() { panel.updateProperties() };
80
81     domView.sidebarPanes.styles.expanded = true;
82
83     domView.sidebarElement = document.createElement("div");
84     domView.sidebarElement.className = "sidebar";
85
86     domView.sidebarElement.appendChild(domView.sidebarPanes.styles.element);
87     domView.sidebarElement.appendChild(domView.sidebarPanes.metrics.element);
88     domView.sidebarElement.appendChild(domView.sidebarPanes.properties.element);
89
90     domView.sideContentElement.appendChild(domView.treeContentElement);
91     domView.sideContentElement.appendChild(domView.crumbsElement);
92     domView.treeContentElement.appendChild(domView.treeListElement);
93
94     domView.sidebarResizeElement = document.createElement("div");
95     domView.sidebarResizeElement.className = "sidebar-resizer-vertical sidebar-resizer-vertical-right";
96     domView.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false);
97
98     domView.contentElement.appendChild(domView.sideContentElement);
99     domView.contentElement.appendChild(domView.sidebarElement);
100     domView.contentElement.appendChild(domView.sidebarResizeElement);
101
102     this.rootDOMNode = this.resource.documentNode;
103 }
104
105 WebInspector.DocumentPanel.prototype = {
106     resize: function()
107     {
108         this.updateTreeSelection();
109         this.updateBreadcrumbSizes();
110     },
111
112     updateTreeSelection: function()
113     {
114         if (!this.views.dom.treeOutline || !this.views.dom.treeOutline.selectedTreeElement)
115             return;
116         var element = this.views.dom.treeOutline.selectedTreeElement;
117         element.updateSelection();
118     },
119
120     get rootDOMNode()
121     {
122         return this._rootDOMNode;
123     },
124
125     set rootDOMNode(x)
126     {
127         if (this._rootDOMNode === x)
128             return;
129
130         this._rootDOMNode = x;
131
132         this.updateBreadcrumb();
133         this.updateTreeOutline();
134     },
135
136     get focusedDOMNode()
137     {
138         return this._focusedDOMNode;
139     },
140
141     set focusedDOMNode(x)
142     {
143         if (this._focusedDOMNode === x) {
144             var nodeItem = this.revealNode(x);
145             if (nodeItem)
146                 nodeItem.select();
147             return;
148         }
149
150         this._focusedDOMNode = x;
151
152         this._focusedNodeChanged();
153
154         var nodeItem = this.revealNode(x);
155         if (nodeItem)
156             nodeItem.select();
157     },
158
159     _focusedNodeChanged: function(forceUpdate)
160     {
161         this.updateBreadcrumb(forceUpdate);
162
163         for (var pane in this.views.dom.sidebarPanes)
164             this.views.dom.sidebarPanes[pane].needsUpdate = true;
165
166         this.updateStyles(forceUpdate);
167         this.updateMetrics();
168         this.updateProperties();
169     },
170
171     revealNode: function(node)
172     {
173         var nodeItem = this.views.dom.treeOutline.findTreeElement(node, this._isAncestorIncludingParentFramesWithinPanel.bind(this), this._parentNodeOrFrameElementWithinPanel.bind(this));
174         if (!nodeItem)
175             return;
176
177         nodeItem.reveal();
178         return nodeItem;
179     },
180
181     updateTreeOutline: function()
182     {
183         this.views.dom.treeOutline.removeChildrenRecursive();
184
185         if (!this.rootDOMNode)
186             return;
187
188         // FIXME: this could use findTreeElement to reuse a tree element if it already exists
189         var node = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.rootDOMNode) : this.rootDOMNode.firstChild);
190         while (node) {
191             this.views.dom.treeOutline.appendChild(new WebInspector.DOMNodeTreeElement(node));
192             node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
193         }
194
195         this.updateTreeSelection();
196     },
197
198     updateBreadcrumb: function(forceUpdate)
199     {
200         if (!this.visible)
201             return;
202
203         var crumbs = this.views.dom.innerCrumbsElement;
204
205         var handled = false;
206         var foundRoot = false;
207         var crumb = crumbs.firstChild;
208         while (crumb) {
209             if (crumb.representedObject === this.rootDOMNode)
210                 foundRoot = true;
211
212             if (foundRoot)
213                 crumb.addStyleClass("dimmed");
214             else
215                 crumb.removeStyleClass("dimmed");
216
217             if (crumb.representedObject === this.focusedDOMNode) {
218                 crumb.addStyleClass("selected");
219                 handled = true;
220             } else {
221                 crumb.removeStyleClass("selected");
222             }
223
224             crumb = crumb.nextSibling;
225         }
226
227         if (handled && !forceUpdate) {
228             // We don't need to rebuild the crumbs, but we need to adjust sizes
229             // to reflect the new focused or root node.
230             this.updateBreadcrumbSizes();
231             return;
232         }
233
234         crumbs.removeChildren();
235
236         var panel = this;
237         var selectCrumbFunction = function(event) {
238             var crumb = event.currentTarget;
239             if (crumb.hasStyleClass("collapsed")) {
240                 // Clicking a collapsed crumb will expose the hidden crumbs.
241                 if (crumb === panel.views.dom.innerCrumbsElement.firstChild) {
242                     // If the focused crumb is the first child, pick the farthest crumb
243                     // that is still hidden. This allows the user to expose every crumb.
244                     var currentCrumb = crumb;
245                     while (currentCrumb) {
246                         var hidden = currentCrumb.hasStyleClass("hidden");
247                         var collapsed = currentCrumb.hasStyleClass("collapsed");
248                         if (!hidden && !collapsed)
249                             break;
250                         crumb = currentCrumb;
251                         currentCrumb = currentCrumb.nextSibling;
252                     }
253                 }
254
255                 panel.updateBreadcrumbSizes(crumb);
256             } else {
257                 // Clicking a dimmed crumb or double clicking (event.detail >= 2)
258                 // will change the root node in addition to the focused node.
259                 if (event.detail >= 2 || crumb.hasStyleClass("dimmed"))
260                     panel.rootDOMNode = crumb.representedObject.parentNode;
261                 panel.focusedDOMNode = crumb.representedObject;
262             }
263
264             event.preventDefault();
265         };
266
267         var mouseOverCrumbFunction = function(event) {
268             panel.mouseOverCrumb = true;
269             InspectorController.highlightDOMNode(this.representedObject);
270
271             if ("mouseOutTimeout" in panel) {
272                 clearTimeout(panel.mouseOutTimeout);
273                 delete panel.mouseOutTimeout;
274             }
275         };
276
277         var mouseOutCrumbFunction = function(event) {
278             delete panel.mouseOverCrumb;
279
280             if ("mouseOutTimeout" in panel) {
281                 clearTimeout(panel.mouseOutTimeout);
282                 delete panel.mouseOutTimeout;
283             }
284
285             var timeoutFunction = function() {
286                 if (!panel.mouseOverCrumb)
287                     panel.updateBreadcrumbSizes();
288             };
289
290             panel.mouseOutTimeout = setTimeout(timeoutFunction, 500);
291         };
292
293         foundRoot = false;
294         for (var current = this.focusedDOMNode; current; current = this._parentNodeOrFrameElementWithinPanel(current)) {
295             if (current === this.resource.documentNode)
296                 break;
297
298             if (current.nodeType === Node.DOCUMENT_NODE)
299                 continue;
300
301             if (current === this.rootDOMNode)
302                 foundRoot = true;
303
304             var crumb = document.createElement("span");
305             crumb.className = "crumb";
306             crumb.representedObject = current;
307             crumb.addEventListener("mousedown", selectCrumbFunction, false);
308             crumb.addEventListener("mouseover", mouseOverCrumbFunction.bind(crumb), false);
309             crumb.addEventListener("mouseout", mouseOutCrumbFunction, false);
310
311             var crumbTitle;
312             switch (current.nodeType) {
313                 case Node.ELEMENT_NODE:
314                     crumbTitle = current.nodeName.toLowerCase();
315
316                     var nameElement = document.createElement("span");
317                     nameElement.textContent = crumbTitle;
318                     crumb.appendChild(nameElement);
319
320                     var idAttribute = current.getAttribute("id");
321                     if (idAttribute) {
322                         var idElement = document.createElement("span");
323                         crumb.appendChild(idElement);
324
325                         var part = "#" + idAttribute;
326                         crumbTitle += part;
327                         idElement.appendChild(document.createTextNode(part));
328
329                         // Mark the name as extra, since the ID is more important.
330                         nameElement.className = "extra";
331                     }
332
333                     var classAttribute = current.getAttribute("class");
334                     if (classAttribute) {
335                         var classes = classAttribute.split(/\s+/);
336                         var foundClasses = {};
337
338                         if (classes.length) {
339                             var classesElement = document.createElement("span");
340                             classesElement.className = "extra";
341                             crumb.appendChild(classesElement);
342
343                             for (var i = 0; i < classes.length; ++i) {
344                                 var className = classes[i];
345                                 if (className && !(className in foundClasses)) {
346                                     var part = "." + className;
347                                     crumbTitle += part;
348                                     classesElement.appendChild(document.createTextNode(part));
349                                     foundClasses[className] = true;
350                                 }
351                             }
352                         }
353                     }
354
355                     break;
356
357                 case Node.TEXT_NODE:
358                     if (isNodeWhitespace.call(current))
359                         crumbTitle = WebInspector.UIString("(whitespace)");
360                     else
361                         crumbTitle = WebInspector.UIString("(text)");
362                     break
363
364                 case Node.COMMENT_NODE:
365                     crumbTitle = "<!-->";
366                     break;
367
368                 default:
369                     crumbTitle = current.nodeName.toLowerCase();
370             }
371
372             if (!crumb.childNodes.length) {
373                 var nameElement = document.createElement("span");
374                 nameElement.textContent = crumbTitle;
375                 crumb.appendChild(nameElement);
376             }
377
378             crumb.title = crumbTitle;
379
380             if (foundRoot)
381                 crumb.addStyleClass("dimmed");
382             if (current === this.focusedDOMNode)
383                 crumb.addStyleClass("selected");
384             if (!crumbs.childNodes.length)
385                 crumb.addStyleClass("end");
386
387             crumbs.appendChild(crumb);
388         }
389
390         if (crumbs.hasChildNodes())
391             crumbs.lastChild.addStyleClass("start");
392
393         this.updateBreadcrumbSizes();
394     },
395
396     updateBreadcrumbSizes: function(focusedCrumb)
397     {
398         if (!this.visible)
399             return;
400
401         if (document.body.offsetWidth <= 0) {
402             // The stylesheet hasn't loaded yet, so we need to update later.
403             setTimeout(this.updateBreadcrumbSizes.bind(this), 0);
404             return;
405         }
406
407         var crumbs = this.views.dom.innerCrumbsElement;
408         if (!crumbs.childNodes.length)
409             return; // No crumbs, do nothing.
410
411         var crumbsContainer = this.views.dom.crumbsElement;
412         if (crumbsContainer.offsetWidth <= 0 || crumbs.offsetWidth <= 0)
413             return;
414
415         // A Zero index is the right most child crumb in the breadcrumb.
416         var selectedIndex = 0;
417         var focusedIndex = 0;
418         var selectedCrumb;
419
420         var i = 0;
421         var crumb = crumbs.firstChild;
422         while (crumb) {
423             // Find the selected crumb and index. 
424             if (!selectedCrumb && crumb.hasStyleClass("selected")) {
425                 selectedCrumb = crumb;
426                 selectedIndex = i;
427             }
428
429             // Find the focused crumb index. 
430             if (crumb === focusedCrumb)
431                 focusedIndex = i;
432
433             // Remove any styles that affect size before
434             // deciding to shorten any crumbs.
435             if (crumb !== crumbs.lastChild)
436                 crumb.removeStyleClass("start");
437             if (crumb !== crumbs.firstChild)
438                 crumb.removeStyleClass("end");
439
440             crumb.removeStyleClass("compact");
441             crumb.removeStyleClass("collapsed");
442             crumb.removeStyleClass("hidden");
443
444             crumb = crumb.nextSibling;
445             ++i;
446         }
447
448         // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs().
449         // The order of the crumbs in the document is opposite of the visual order.
450         crumbs.firstChild.addStyleClass("end");
451         crumbs.lastChild.addStyleClass("start");
452
453         function crumbsAreSmallerThanContainer()
454         {
455             // There is some fixed extra space that is not returned in the crumbs' offsetWidth.
456             // This padding is added to the crumbs' offsetWidth when comparing to the crumbsContainer.
457             var rightPadding = 9;
458             return ((crumbs.offsetWidth + rightPadding) < crumbsContainer.offsetWidth);
459         }
460
461         if (crumbsAreSmallerThanContainer())
462             return; // No need to compact the crumbs, they all fit at full size.
463
464         var BothSides = 0;
465         var AncestorSide = -1;
466         var ChildSide = 1;
467
468         function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb)
469         {
470             if (!significantCrumb)
471                 significantCrumb = (focusedCrumb || selectedCrumb);
472
473             if (significantCrumb === selectedCrumb)
474                 var significantIndex = selectedIndex;
475             else if (significantCrumb === focusedCrumb)
476                 var significantIndex = focusedIndex;
477             else {
478                 var significantIndex = 0;
479                 for (var i = 0; i < crumbs.childNodes.length; ++i) {
480                     if (crumbs.childNodes[i] === significantCrumb) {
481                         significantIndex = i;
482                         break;
483                     }
484                 }
485             }
486
487             function shrinkCrumbAtIndex(index)
488             {
489                 var shrinkCrumb = crumbs.childNodes[index];
490                 if (shrinkCrumb && shrinkCrumb !== significantCrumb)
491                     shrinkingFunction(shrinkCrumb);
492                 if (crumbsAreSmallerThanContainer())
493                     return true; // No need to compact the crumbs more.
494                 return false;
495             }
496
497             // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
498             // fit in the crumbsContainer or we run out of crumbs to shrink.
499             if (direction) {
500                 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
501                 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
502                 while (index !== significantIndex) {
503                     if (shrinkCrumbAtIndex(index))
504                         return true;
505                     index += (direction > 0 ? 1 : -1);
506                 }
507             } else {
508                 // Crumbs are shrunk in order of descending distance from the signifcant crumb,
509                 // with a tie going to child crumbs.
510                 var startIndex = 0;
511                 var endIndex = crumbs.childNodes.length - 1;
512                 while (startIndex != significantIndex || endIndex != significantIndex) {
513                     var startDistance = significantIndex - startIndex;
514                     var endDistance = endIndex - significantIndex;
515                     if (startDistance >= endDistance)
516                         var index = startIndex++;
517                     else
518                         var index = endIndex--;
519                     if (shrinkCrumbAtIndex(index))
520                         return true;
521                 }
522             }
523
524             // We are not small enough yet, return false so the caller knows.
525             return false;
526         }
527
528         function coalesceCollapsedCrumbs()
529         {
530             var crumb = crumbs.firstChild;
531             var collapsedRun = false;
532             var newStartNeeded = false;
533             var newEndNeeded = false;
534             while (crumb) {
535                 var hidden = crumb.hasStyleClass("hidden");
536                 if (!hidden) {
537                     var collapsed = crumb.hasStyleClass("collapsed"); 
538                     if (collapsedRun && collapsed) {
539                         crumb.addStyleClass("hidden");
540                         crumb.removeStyleClass("compact");
541                         crumb.removeStyleClass("collapsed");
542
543                         if (crumb.hasStyleClass("start")) {
544                             crumb.removeStyleClass("start");
545                             newStartNeeded = true;
546                         }
547
548                         if (crumb.hasStyleClass("end")) {
549                             crumb.removeStyleClass("end");
550                             newEndNeeded = true;
551                         }
552
553                         continue;
554                     }
555
556                     collapsedRun = collapsed;
557
558                     if (newEndNeeded) {
559                         newEndNeeded = false;
560                         crumb.addStyleClass("end");
561                     }
562                 } else
563                     collapsedRun = true;
564                 crumb = crumb.nextSibling;
565             }
566
567             if (newStartNeeded) {
568                 crumb = crumbs.lastChild;
569                 while (crumb) {
570                     if (!crumb.hasStyleClass("hidden")) {
571                         crumb.addStyleClass("start");
572                         break;
573                     }
574                     crumb = crumb.previousSibling;
575                 }
576             }
577         }
578
579         function compact(crumb)
580         {
581             if (crumb.hasStyleClass("hidden"))
582                 return;
583             crumb.addStyleClass("compact");
584         }
585
586         function collapse(crumb, dontCoalesce)
587         {
588             if (crumb.hasStyleClass("hidden"))
589                 return;
590             crumb.addStyleClass("collapsed");
591             crumb.removeStyleClass("compact");
592             if (!dontCoalesce)
593                 coalesceCollapsedCrumbs();
594         }
595
596         function compactDimmed(crumb)
597         {
598             if (crumb.hasStyleClass("dimmed"))
599                 compact(crumb);
600         }
601
602         function collapseDimmed(crumb)
603         {
604             if (crumb.hasStyleClass("dimmed"))
605                 collapse(crumb);
606         }
607
608         if (!focusedCrumb) {
609             // When not focused on a crumb we can be biased and collapse less important
610             // crumbs that the user might not care much about.
611
612             // Compact child crumbs.
613             if (makeCrumbsSmaller(compact, ChildSide))
614                 return;
615
616             // Collapse child crumbs.
617             if (makeCrumbsSmaller(collapse, ChildSide))
618                 return;
619
620             // Compact dimmed ancestor crumbs.
621             if (makeCrumbsSmaller(compactDimmed, AncestorSide))
622                 return;
623
624             // Collapse dimmed ancestor crumbs.
625             if (makeCrumbsSmaller(collapseDimmed, AncestorSide))
626                 return;
627         }
628
629         // Compact ancestor crumbs, or from both sides if focused.
630         if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide)))
631             return;
632
633         // Collapse ancestor crumbs, or from both sides if focused.
634         if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide)))
635             return;
636
637         if (!selectedCrumb)
638             return;
639
640         // Compact the selected crumb.
641         compact(selectedCrumb);
642         if (crumbsAreSmallerThanContainer())
643             return;
644
645         // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
646         collapse(selectedCrumb, true);
647     },
648
649     updateStyles: function(forceUpdate)
650     {
651         var stylesSidebarPane = this.views.dom.sidebarPanes.styles;
652         if (!stylesSidebarPane.expanded || !stylesSidebarPane.needsUpdate)
653             return;
654
655         stylesSidebarPane.update(this.focusedDOMNode, undefined, forceUpdate);
656         stylesSidebarPane.needsUpdate = false;
657     },
658
659     updateMetrics: function()
660     {
661         var metricsSidebarPane = this.views.dom.sidebarPanes.metrics;
662         if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate)
663             return;
664
665         metricsSidebarPane.update(this.focusedDOMNode);
666         metricsSidebarPane.needsUpdate = false;
667     },
668
669     updateProperties: function()
670     {
671         var propertiesSidebarPane = this.views.dom.sidebarPanes.properties;
672         if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate)
673             return;
674
675         propertiesSidebarPane.update(this.focusedDOMNode);
676         propertiesSidebarPane.needsUpdate = false;
677     },
678
679     handleKeyEvent: function(event)
680     {
681         if (this.views.dom.treeOutline && this.currentView && this.currentView === this.views.dom)
682             this.views.dom.treeOutline.handleKeyEvent(event);
683     },
684
685     handleCopyEvent: function(event)
686     {
687         if (this.currentView !== this.views.dom)
688             return;
689
690         // Don't prevent the normal copy if the user has a selection.
691         if (!window.getSelection().isCollapsed)
692             return;
693
694         switch (this.focusedDOMNode.nodeType) {
695             case Node.ELEMENT_NODE:
696                 var data = this.focusedDOMNode.outerHTML;
697                 break;
698
699             case Node.COMMENT_NODE:
700                 var data = "<!--" + this.focusedDOMNode.nodeValue + "-->";
701                 break;
702
703             default:
704             case Node.TEXT_NODE:
705                 var data = this.focusedDOMNode.nodeValue;
706         }
707
708         event.clipboardData.clearData();
709         event.preventDefault();
710
711         if (data)
712             event.clipboardData.setData("text/plain", data);
713     },
714
715     rightSidebarResizerDragStart: function(event)
716     {
717         if (this.sidebarDragEventListener || this.sidebarDragEndEventListener)
718             return this.rightSidebarResizerDragEnd(event);
719
720         this.sidebarDragEventListener = this.rightSidebarResizerDrag.bind(this);
721         this.sidebarDragEndEventListener = this.rightSidebarResizerDragEnd.bind(this);
722         WebInspector.elementDragStart(this.views.dom.sidebarElement, this.sidebarDragEventListener, this.sidebarDragEndEventListener, event, "col-resize");
723     },
724
725     rightSidebarResizerDragEnd: function(event)
726     {
727         WebInspector.elementDragEnd(this.views.dom.sidebarElement, this.sidebarDragEventListener, this.sidebarDragEndEventListener, event);
728         delete this.sidebarDragEventListener;
729         delete this.sidebarDragEndEventListener;
730     },
731
732     rightSidebarResizerDrag: function(event)
733     {
734         var x = event.pageX;
735
736         var leftSidebarWidth = document.getElementById("sidebar").offsetWidth;
737         var newWidth = Number.constrain(window.innerWidth - x, 100, window.innerWidth - leftSidebarWidth - 100);
738
739         this.views.dom.sidebarElement.style.width = newWidth + "px";
740         this.views.dom.sideContentElement.style.right = newWidth + "px";
741         this.views.dom.sidebarResizeElement.style.right = (newWidth - 3) + "px";
742
743         this.updateTreeSelection();
744         this.updateBreadcrumbSizes();
745
746         event.preventDefault();
747     },
748
749     _getDocumentForNode: function(node)
750     {
751         return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument;
752     },
753
754     _parentNodeOrFrameElementWithinPanel: function(node)
755     {
756         var parent = node.parentNode;
757         if (parent)
758             return parent;
759
760         var document = this._getDocumentForNode(node);
761
762         if (document === this.resource.documentNode)
763             return undefined;
764
765         return document.defaultView.frameElement;
766     },
767
768     _isAncestorIncludingParentFramesWithinPanel: function(a, b)
769     {
770         for (var node = b; node; node = this._getDocumentForNode(node).defaultView.frameElement) {
771             if (isAncestorNode.call(a, node))
772                 return true;
773
774             if (this._getDocumentForNode(node) === this.resource.documentNode) {
775                 // We've gone as high in the frame hierarchy as we can without
776                 // moving out of this DocumentPanel.
777                 return false;
778             }
779         }
780
781         return false;
782     },
783
784     _treeElementFromEvent: function(event)
785     {
786         var outline = this.views.dom.treeOutline;
787
788         var root = this.views.dom.treeListElement;
789
790         // We choose this X coordinate based on the knowledge that our list
791         // items extend nearly to the right edge of the outer <ol>.
792         var x = root.totalOffsetLeft + root.offsetWidth - 20;
793
794         var y = event.pageY;
795
796         // Our list items have 1-pixel cracks between them vertically. We avoid
797         // the cracks by checking slightly above and slightly below the mouse
798         // and seeing if we hit the same element each time.
799         var elementUnderMouse = outline.treeElementFromPoint(x, y);
800         var elementAboveMouse = outline.treeElementFromPoint(x, y - 2);
801         var element;
802         if (elementUnderMouse === elementAboveMouse)
803             element = elementUnderMouse;
804         else
805             element = outline.treeElementFromPoint(x, y + 2);
806
807         return element;
808     },
809
810     _ondblclick: function(event)
811     {
812         var element = this._treeElementFromEvent(event);
813
814         if (!element)
815             return;
816
817         element.ondblclick();
818     },
819
820     _onmousedown: function(event)
821     {
822         var element = this._treeElementFromEvent(event);
823
824         if (!element || element.isEventWithinDisclosureTriangle(event))
825             return;
826
827         element.select();
828     },
829
830     _onmousemove: function(event)
831     {
832         var element = this._treeElementFromEvent(event);
833         if (!element)
834             return;
835
836         InspectorController.highlightDOMNode(element.representedObject);
837     },
838 }
839
840 WebInspector.DocumentPanel.prototype.__proto__ = WebInspector.SourcePanel.prototype;
841
842 WebInspector.DOMNodeTreeElement = function(node)
843 {
844     var hasChildren = node.contentDocument || (Preferences.ignoreWhitespace ? (firstChildSkippingWhitespace.call(node) ? true : false) : node.hasChildNodes());
845     var titleInfo = nodeTitleInfo.call(node, hasChildren, WebInspector.linkifyURL);
846
847     if (titleInfo.hasChildren) 
848         this.whitespaceIgnored = Preferences.ignoreWhitespace;
849
850     TreeElement.call(this, titleInfo.title, node, titleInfo.hasChildren);
851 }
852
853 WebInspector.DOMNodeTreeElement.prototype = {
854     updateSelection: function()
855     {
856         var listItemElement = this.listItemElement;
857         if (!listItemElement)
858             return;
859
860         if (document.body.offsetWidth <= 0) {
861             // The stylesheet hasn't loaded yet, so we need to update later.
862             setTimeout(this.updateSelection.bind(this), 0);
863             return;
864         }
865
866         if (!this.selectionElement) {
867             this.selectionElement = document.createElement("div");
868             this.selectionElement.className = "selection selected";
869             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
870         }
871
872         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
873     },
874
875     onattach: function()
876     {
877         this.listItemElement.addEventListener("mousedown", this.onmousedown.bind(this), false);
878
879         this._makeURLsActivateOnModifiedClick();
880     },
881
882     _makeURLsActivateOnModifiedClick: function()
883     {
884         var links = this.listItemElement.querySelectorAll("li > .webkit-html-tag > .webkit-html-attribute > .webkit-html-external-link, li > .webkit-html-tag > .webkit-html-attribute > .webkit-html-resource-link");
885         if (!links)
886             return;
887
888         var isMac = InspectorController.platform().indexOf("mac") == 0;
889
890         for (var i = 0; i < links.length; ++i) {
891             var link = links[i];
892             var isExternal = link.hasStyleClass("webkit-html-external-link");
893             var href = link.getAttribute("href");
894             var title;
895             if (isMac) {
896                 if (isExternal)
897                     title = WebInspector.UIString("Option-click to visit %s.", href);
898                 else
899                     title = WebInspector.UIString("Option-click to show %s.", href);
900             } else {
901                 if (isExternal)
902                     title = WebInspector.UIString("Alt-click to visit %s.", href);
903                 else
904                     title = WebInspector.UIString("Alt-click to show %s.", href);
905             }
906             link.setAttribute("title", title);
907             link.followOnAltClick = true;
908         }
909     },
910
911     onpopulate: function()
912     {
913         if (this.children.length || this.whitespaceIgnored !== Preferences.ignoreWhitespace)
914             return;
915
916         this.removeChildren();
917         this.whitespaceIgnored = Preferences.ignoreWhitespace;
918
919         var treeElement = this;
920         function appendChildrenOfNode(node)
921         {
922             var child = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(node) : node.firstChild);
923             while (child) {
924                 treeElement.appendChild(new WebInspector.DOMNodeTreeElement(child));
925                 child = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(child) : child.nextSibling;
926             }
927         }
928
929         if (this.representedObject.contentDocument)
930             appendChildrenOfNode(this.representedObject.contentDocument);
931
932         appendChildrenOfNode(this.representedObject);
933
934         if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
935             var title = "<span class=\"webkit-html-tag close\">&lt;/" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "&gt;</span>";
936             var item = new TreeElement(title, this.representedObject, false);
937             item.selectable = false;
938             this.appendChild(item);
939         }
940     },
941
942     onexpand: function()
943     {
944         this.treeOutline.panel.updateTreeSelection();
945     },
946
947     oncollapse: function()
948     {
949         this.treeOutline.panel.updateTreeSelection();
950     },
951
952     onreveal: function()
953     {
954         if (this.listItemElement)
955             this.listItemElement.scrollIntoViewIfNeeded(false);
956     },
957
958     onselect: function()
959     {
960         this._selectedByCurrentMouseDown = true;
961         this.treeOutline.panel.focusedDOMNode = this.representedObject;
962         this.updateSelection();
963     },
964
965     onmousedown: function(event)
966     {
967         if (this._editing)
968             return;
969
970         if (this._selectedByCurrentMouseDown)
971             delete this._selectedByCurrentMouseDown;
972         else if (this._startEditing(event)) {
973             event.preventDefault();
974             return;
975         }
976
977         // Prevent selecting the nearest word on double click.
978         if (event.detail >= 2)
979             event.preventDefault();
980     },
981
982     ondblclick: function(treeElement, event)
983     {
984         if (this._editing)
985             return;
986
987         var panel = this.treeOutline.panel;
988         panel.rootDOMNode = this.parent.representedObject;
989         panel.focusedDOMNode = this.representedObject;
990
991         if (this.hasChildren && !this.expanded)
992             this.expand();
993     },
994
995     _startEditing: function(event)
996     {
997         if (this.treeOutline.panel.focusedDOMNode != this.representedObject)
998             return;
999
1000         if (this.representedObject.nodeType != Node.ELEMENT_NODE && this.representedObject.nodeType != Node.TEXT_NODE)
1001             return false;
1002
1003         var textNode = event.target.enclosingNodeOrSelfWithClass("webkit-html-text-node");
1004         if (textNode)
1005             return this._startEditingTextNode(textNode);
1006
1007         var attribute = event.target.enclosingNodeOrSelfWithClass("webkit-html-attribute");
1008         if (attribute)
1009             return this._startEditingAttribute(attribute, event);
1010
1011         return false;
1012     },
1013
1014     _startEditingAttribute: function(attribute, event)
1015     {
1016         if (WebInspector.isBeingEdited(attribute))
1017             return true;
1018
1019         var attributeNameElement = attribute.getElementsByClassName("webkit-html-attribute-name")[0];
1020         if (!attributeNameElement)
1021             return false;
1022
1023         var isURL = event.target.enclosingNodeOrSelfWithClass("webkit-html-external-link") || event.target.enclosingNodeOrSelfWithClass("webkit-html-resource-link");
1024         if (isURL && event.altKey)
1025             return false;
1026
1027         var attributeName = attributeNameElement.innerText;
1028
1029         function removeZeroWidthSpaceRecursive(node)
1030         {
1031             if (node.nodeType === Node.TEXT_NODE) {
1032                 node.nodeValue = node.nodeValue.replace(/\u200B/g, "");
1033                 return;
1034             }
1035
1036             if (node.nodeType !== Node.ELEMENT_NODE)
1037                 return;
1038
1039             for (var child = node.firstChild; child; child = child.nextSibling)
1040                 removeZeroWidthSpaceRecursive(child);
1041         }
1042
1043         // Remove zero-width spaces that were added by nodeTitleInfo.
1044         removeZeroWidthSpaceRecursive(attribute);
1045
1046         this._editing = true;
1047
1048         WebInspector.startEditing(attribute, this._attributeEditingCommitted.bind(this), this._editingCancelled.bind(this), attributeName);
1049         window.getSelection().setBaseAndExtent(event.target, 0, event.target, 1);
1050
1051         return true;
1052     },
1053
1054     _startEditingTextNode: function(textNode)
1055     {
1056         if (WebInspector.isBeingEdited(textNode))
1057             return true;
1058
1059         this._editing = true;
1060
1061         WebInspector.startEditing(textNode, this._textNodeEditingCommitted.bind(this), this._editingCancelled.bind(this));
1062         window.getSelection().setBaseAndExtent(textNode, 0, textNode, 1);
1063
1064         return true;
1065     },
1066
1067     _attributeEditingCommitted: function(element, newText, oldText, attributeName)
1068     {
1069         delete this._editing;
1070
1071         var parseContainerElement = document.createElement("span");
1072         parseContainerElement.innerHTML = "<span " + newText + "></span>";
1073         var parseElement = parseContainerElement.firstChild;
1074         if (!parseElement || !parseElement.hasAttributes()) {
1075             editingCancelled(element, context);
1076             return;
1077         }
1078
1079         var foundOriginalAttribute = false;
1080         for (var i = 0; i < parseElement.attributes.length; ++i) {
1081             var attr = parseElement.attributes[i];
1082             foundOriginalAttribute = foundOriginalAttribute || attr.name === attributeName;
1083             Element.prototype.setAttribute.call(this.representedObject, attr.name, attr.value);
1084         }
1085
1086         if (!foundOriginalAttribute)
1087             Element.prototype.removeAttribute.call(this.representedObject, attributeName);
1088
1089         this._updateTitle();
1090         this.treeOutline.panel._focusedNodeChanged(true);
1091     },
1092
1093     _textNodeEditingCommitted: function(element, newText)
1094     {
1095         delete this._editing;
1096
1097         var textNode;
1098         if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
1099             // We only show text nodes inline in elements if the element only
1100             // has a single child, and that child is a text node.
1101             textNode = this.representedObject.firstChild;
1102         } else if (this.representedObject.nodeType == Node.TEXT_NODE)
1103             textNode = this.representedObject;
1104
1105         textNode.nodeValue = newText;
1106         this._updateTitle();
1107     },
1108
1109
1110     _editingCancelled: function(element, context)
1111     {
1112         delete this._editing;
1113
1114         this._updateTitle();
1115     },
1116
1117     _updateTitle: function()
1118     {
1119         this.title = nodeTitleInfo.call(this.representedObject, this.hasChildren, WebInspector.linkifyURL).title;
1120         delete this.selectionElement;
1121         this.updateSelection();
1122         this._makeURLsActivateOnModifiedClick();
1123     },
1124 }
1125
1126 WebInspector.DOMNodeTreeElement.prototype.__proto__ = TreeElement.prototype;