Reviewed by Mark Rowe.
[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: "DOM" }];
32     if (views)
33         allViews = allViews.concat(views);
34
35     WebInspector.SourcePanel.call(this, resource, allViews);
36
37     var domView = this.views.dom;
38     domView.show = function() { InspectorController.highlightDOMNode(panel.focusedDOMNode) };
39     domView.hide = function() { InspectorController.hideDOMNodeHighlight() };
40
41     domView.sideContentElement = document.createElement("div");
42     domView.sideContentElement.className = "content side";
43
44     domView.treeContentElement = document.createElement("div");
45     domView.treeContentElement.className = "content tree outline-disclosure";
46
47     domView.treeListElement = document.createElement("ol");
48     domView.treeOutline = new TreeOutline(domView.treeListElement);
49     domView.treeOutline.panel = this;
50
51     domView.crumbsElement = document.createElement("div");
52     domView.crumbsElement.className = "crumbs";
53
54     domView.innerCrumbsElement = document.createElement("div");
55     domView.crumbsElement.appendChild(domView.innerCrumbsElement);
56
57     domView.sidebarPanes = {};
58     domView.sidebarPanes.styles = new WebInspector.StylesSidebarPane();
59     domView.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
60     domView.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
61
62     var panel = this;
63     domView.sidebarPanes.styles.onexpand = function() { panel.updateStyles() };
64     domView.sidebarPanes.metrics.onexpand = function() { panel.updateMetrics() };
65     domView.sidebarPanes.properties.onexpand = function() { panel.updateProperties() };
66
67     domView.sidebarPanes.styles.expanded = true;
68
69     domView.sidebarElement = document.createElement("div");
70     domView.sidebarElement.className = "sidebar";
71
72     domView.sidebarElement.appendChild(domView.sidebarPanes.styles.element);
73     domView.sidebarElement.appendChild(domView.sidebarPanes.metrics.element);
74     domView.sidebarElement.appendChild(domView.sidebarPanes.properties.element);
75
76     domView.sideContentElement.appendChild(domView.treeContentElement);
77     domView.sideContentElement.appendChild(domView.crumbsElement);
78     domView.treeContentElement.appendChild(domView.treeListElement);
79
80     domView.sidebarResizeElement = document.createElement("div");
81     domView.sidebarResizeElement.className = "sidebar-resizer-vertical sidebar-resizer-vertical-right";
82     domView.sidebarResizeElement.addEventListener("mousedown", function(event) { panel.rightSidebarResizerDragStart(event) }, false);
83
84     domView.contentElement.appendChild(domView.sideContentElement);
85     domView.contentElement.appendChild(domView.sidebarElement);
86     domView.contentElement.appendChild(domView.sidebarResizeElement);
87
88     this.rootDOMNode = this.resource.documentNode;
89 }
90
91 WebInspector.DocumentPanel.prototype = {
92     show: function()
93     {
94         WebInspector.SourcePanel.prototype.show.call(this);
95         this.updateTreeSelection();
96     },
97
98     resize: function()
99     {
100         this.updateTreeSelection();
101     },
102
103     updateTreeSelection: function()
104     {
105         if (!this.views.dom.treeOutline || !this.views.dom.treeOutline.selectedTreeElement)
106             return;
107         var element = this.views.dom.treeOutline.selectedTreeElement;
108         element.updateSelection();
109     },
110
111     get rootDOMNode()
112     {
113         return this._rootDOMNode;
114     },
115
116     set rootDOMNode(x)
117     {
118         if (this._rootDOMNode === x)
119             return;
120
121         this._rootDOMNode = x;
122
123         this.updateBreadcrumb();
124         this.updateTreeOutline();
125     },
126
127     get focusedDOMNode()
128     {
129         return this._focusedDOMNode;
130     },
131
132     set focusedDOMNode(x)
133     {
134         if (this.resource.category !== WebInspector.resourceCategories.documents) {
135             InspectorController.log("Called set focusedDOMNode on a non-document resource " + this.resource.displayName + " which is not a document");
136             return;
137         }
138
139         if (this._focusedDOMNode === x) {
140             var nodeItem = this.revealNode(x);
141             if (nodeItem)
142                 nodeItem.select();
143             return;
144         }
145
146         this._focusedDOMNode = x;
147
148         this.updateBreadcrumb();
149
150         for (var pane in this.views.dom.sidebarPanes)
151             this.views.dom.sidebarPanes[pane].needsUpdate = true;
152
153         this.updateStyles();
154         this.updateMetrics();
155         this.updateProperties();
156
157         InspectorController.highlightDOMNode(x);
158
159         var nodeItem = this.revealNode(x);
160         if (nodeItem)
161             nodeItem.select();
162     },
163
164     revealNode: function(node)
165     {
166         var nodeItem = this.views.dom.treeOutline.findTreeElement(node, function(a, b) { return isAncestorNode.call(a, b); }, function(a) { return a.parentNode; });
167         if (!nodeItem)
168             return;
169
170         nodeItem.reveal();
171         return nodeItem;
172     },
173
174     updateTreeOutline: function()
175     {
176         this.views.dom.treeOutline.removeChildrenRecursive();
177
178         if (!this.rootDOMNode)
179             return;
180
181         // FIXME: this could use findTreeElement to reuse a tree element if it already exists
182         var node = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.rootDOMNode) : this.rootDOMNode.firstChild);
183         while (node) {
184             this.views.dom.treeOutline.appendChild(new WebInspector.DOMNodeTreeElement(node));
185             node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
186         }
187
188         this.updateTreeSelection();
189     },
190
191     updateBreadcrumb: function()
192     {
193         if (!this.views || !this.views.dom.contentElement)
194             return;
195         var crumbs = this.views.dom.innerCrumbsElement;
196         if (!crumbs)
197             return;
198
199         var handled = false;
200         var foundRoot = false;
201         var crumb = crumbs.firstChild;
202         while (crumb) {
203             if (crumb.representedObject === this.rootDOMNode)
204                 foundRoot = true;
205
206             if (foundRoot)
207                 crumb.addStyleClass("hidden");
208             else
209                 crumb.removeStyleClass("hidden");
210
211             if (crumb.representedObject === this.focusedDOMNode) {
212                 crumb.addStyleClass("selected");
213                 handled = true;
214             } else {
215                 crumb.removeStyleClass("selected");
216             }
217
218             crumb = crumb.nextSibling;
219         }
220
221         if (handled)
222             return;
223
224         crumbs.removeChildren();
225
226         var panel = this;
227         var selectCrumbFunction = function(event)
228         {
229             if (event.currentTarget.hasStyleClass("hidden"))
230                 panel.rootDOMNode = event.currentTarget.representedObject.parentNode;
231             panel.focusedDOMNode = event.currentTarget.representedObject;
232             event.preventDefault();
233             event.stopPropagation();
234         }
235
236         var selectCrumbRootFunction = function(event)
237         {
238             panel.rootDOMNode = event.currentTarget.representedObject.parentNode;
239             panel.focusedDOMNode = event.currentTarget.representedObject;
240             event.preventDefault();
241             event.stopPropagation();
242         }
243
244         foundRoot = false;
245         var current = this.focusedDOMNode;
246         while (current) {
247             if (current.nodeType === Node.DOCUMENT_NODE)
248                 break;
249
250             if (current === this.rootDOMNode)
251                 foundRoot = true;
252
253             var crumb = document.createElement("span");
254             crumb.className = "crumb";
255             crumb.representedObject = current;
256             crumb.addEventListener("mousedown", selectCrumbFunction, false);
257             crumb.addEventListener("dblclick", selectCrumbRootFunction, false);
258
259             var crumbTitle;
260             switch (current.nodeType) {
261                 case Node.ELEMENT_NODE:
262                     crumbTitle = current.nodeName.toLowerCase();
263     
264                     var value = current.getAttribute("id");
265                     if (value && value.length)
266                         crumbTitle += "#" + value;
267
268                     value = current.getAttribute("class");
269                     if (value && value.length) {
270                         var classes = value.split(/\s+/);
271                         var classesLength = classes.length;
272                         for (var i = 0; i < classesLength; ++i) {
273                             value = classes[i];
274                             if (value && value.length)
275                                 crumbTitle += "." + value;
276                         }
277                     }
278
279                     break;
280
281                 case Node.TEXT_NODE:
282                     if (isNodeWhitespace.call(current))
283                         crumbTitle = "(whitespace)";
284                     else
285                         crumbTitle = "(text)";
286                     break
287
288                 case Node.COMMENT_NODE:
289                     crumbTitle = "<!-->";
290                     break;
291
292                 default:
293                     crumbTitle = current.nodeName.toLowerCase();
294             }
295
296             crumb.textContent = crumbTitle;
297
298             if (foundRoot)
299                 crumb.addStyleClass("hidden");
300             if (current === this.focusedDOMNode)
301                 crumb.addStyleClass("selected");
302             if (!crumbs.childNodes.length)
303                 crumb.addStyleClass("end");
304             if (current.parentNode.nodeType === Node.DOCUMENT_NODE)
305                 crumb.addStyleClass("start");
306
307             crumbs.appendChild(crumb);
308             current = current.parentNode;
309         }
310     },
311
312     updateStyles: function()
313     {
314         var stylesSidebarPane = this.views.dom.sidebarPanes.styles;
315         if (!stylesSidebarPane.expanded || !stylesSidebarPane.needsUpdate)
316             return;
317
318         stylesSidebarPane.update(this.focusedDOMNode);
319         stylesSidebarPane.needsUpdate = false;
320     },
321
322     updateMetrics: function()
323     {
324         var metricsSidebarPane = this.views.dom.sidebarPanes.metrics;
325         if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate)
326             return;
327
328         metricsSidebarPane.update(this.focusedDOMNode);
329         metricsSidebarPane.needsUpdate = false;
330     },
331
332     updateProperties: function()
333     {
334         var propertiesSidebarPane = this.views.dom.sidebarPanes.properties;
335         if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate)
336             return;
337
338         propertiesSidebarPane.update(this.focusedDOMNode);
339         propertiesSidebarPane.needsUpdate = false;
340     },
341
342     handleKeyEvent: function(event)
343     {
344         if (this.views.dom.treeOutline && this.currentView && this.currentView === this.views.dom)
345             this.views.dom.treeOutline.handleKeyEvent(event);
346     },
347
348     handleCopyEvent: function(event)
349     {
350         if (this.currentView !== this.views.dom)
351             return;
352
353         // Don't prevent the normal copy if the user has a selection.
354         if (!window.getSelection().isCollapsed)
355             return;
356
357         switch (this.focusedDOMNode.nodeType) {
358             case Node.ELEMENT_NODE:
359                 var data = this.focusedDOMNode.outerHTML;
360                 break;
361
362             case Node.COMMENT_NODE:
363                 var data = "<!--" + this.focusedDOMNode.nodeValue + "-->";
364                 break;
365
366             default:
367             case Node.TEXT_NODE:
368                 var data = this.focusedDOMNode.nodeValue;
369         }
370
371         event.clipboardData.clearData();
372         event.preventDefault();
373
374         if (data)
375             event.clipboardData.setData("text/plain", data);
376     },
377
378     rightSidebarResizerDragStart: function(event)
379     {
380         var panel = this; 
381         WebInspector.dividerDragStart(this.views.dom.sidebarElement, function(event) { panel.rightSidebarResizerDrag(event) }, function(event) { panel.rightSidebarResizerDragEnd(event) }, event, "col-resize");
382     },
383
384     rightSidebarResizerDragEnd: function(event)
385     {
386         var panel = this;
387         WebInspector.dividerDragEnd(this.views.dom.sidebarElement, function(event) { panel.rightSidebarResizerDrag(event) }, function(event) { panel.rightSidebarResizerDragEnd(event) }, event);
388     },
389
390     rightSidebarResizerDrag: function(event)
391     {
392         var rightSidebar = this.views.dom.sidebarElement;
393         if (rightSidebar.dragging == true) {
394             var x = event.clientX + window.scrollX;
395
396             var leftSidebarWidth = window.getComputedStyle(document.getElementById("sidebar")).getPropertyCSSValue("width").getFloatValue(CSSPrimitiveValue.CSS_PX);
397             var newWidth = Number.constrain(window.innerWidth - x, 100, window.innerWidth - leftSidebarWidth - 100);
398
399             if (x == newWidth)
400                 rightSidebar.dragLastX = x;
401
402             rightSidebar.style.width = newWidth + "px";
403             this.views.dom.sideContentElement.style.right = newWidth + "px";
404             this.views.dom.sidebarResizeElement.style.right = (newWidth - 3) + "px";
405
406             this.updateTreeSelection();
407
408             event.preventDefault();
409         }
410     }
411 }
412
413 WebInspector.DocumentPanel.prototype.__proto__ = WebInspector.SourcePanel.prototype;
414
415 WebInspector.DOMNodeTreeElement = function(node)
416 {
417     var hasChildren = (Preferences.ignoreWhitespace ? (firstChildSkippingWhitespace.call(node) ? true : false) : node.hasChildNodes());
418     var titleInfo = nodeTitleInfo.call(node, hasChildren, WebInspector.linkifyURL);
419
420     if (titleInfo.hasChildren) 
421         this.whitespaceIgnored = Preferences.ignoreWhitespace;
422
423     TreeElement.call(this, titleInfo.title, node, titleInfo.hasChildren);
424 }
425
426 WebInspector.DOMNodeTreeElement.prototype = {
427     updateSelection: function()
428     {
429         var listItemElement = this.listItemElement;
430         if (!listItemElement)
431             return;
432
433         if (!this.selectionElement) {
434             this.selectionElement = document.createElement("div");
435             this.selectionElement.className = "selection selected";
436             listItemElement.insertBefore(this.selectionElement, listItemElement.firstChild);
437         }
438
439         this.selectionElement.style.height = listItemElement.offsetHeight + "px";
440     },
441
442     onpopulate: function()
443     {
444         if (this.children.length || this.whitespaceIgnored !== Preferences.ignoreWhitespace)
445             return;
446
447         this.removeChildren();
448         this.whitespaceIgnored = Preferences.ignoreWhitespace;
449
450         var node = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.representedObject) : this.representedObject.firstChild);
451         while (node) {
452             this.appendChild(new WebInspector.DOMNodeTreeElement(node));
453             node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
454         }
455
456         if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
457             var title = "<span class=\"webkit-html-tag close\">&lt;/" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "&gt;</span>";
458             var item = new TreeElement(title, this.representedObject, false);
459             item.selectable = false;
460             this.appendChild(item);
461         }
462     },
463
464     onexpand: function()
465     {
466         this.treeOutline.panel.updateTreeSelection();
467     },
468
469     oncollapse: function()
470     {
471         this.treeOutline.panel.updateTreeSelection();
472     },
473
474     onreveal: function()
475     {
476         if (!this.listItemElement || !this.treeOutline)
477             return;
478         this.treeOutline.panel.views.dom.treeContentElement.scrollToElement(this.listItemElement);
479     },
480
481     onselect: function()
482     {
483         this.treeOutline.panel.focusedDOMNode = this.representedObject;
484
485         // Call updateSelection twice to make sure the height is correct,
486         // the first time might have a bad height because we are in a weird tree state
487         this.updateSelection();
488
489         var element = this;
490         setTimeout(function() { element.updateSelection() }, 0);
491     },
492
493     ondblclick: function()
494     {
495         var panel = this.treeOutline.panel;
496         panel.rootDOMNode = this.representedObject.parentNode;
497         panel.focusedDOMNode = this.representedObject;
498
499         if (this.hasChildren && !this.expanded)
500             this.expand();
501     }
502 }
503
504 WebInspector.DOMNodeTreeElement.prototype.__proto__ = TreeElement.prototype;