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