Reviewed by Adam.
[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     rightSidebarResizerDragStart: function(event)
349     {
350         var panel = this; 
351         WebInspector.dividerDragStart(this.views.dom.sidebarElement, function(event) { panel.rightSidebarResizerDrag(event) }, function(event) { panel.rightSidebarResizerDragEnd(event) }, event, "col-resize");
352     },
353
354     rightSidebarResizerDragEnd: function(event)
355     {
356         var panel = this;
357         WebInspector.dividerDragEnd(this.views.dom.sidebarElement, function(event) { panel.rightSidebarResizerDrag(event) }, function(event) { panel.rightSidebarResizerDragEnd(event) }, event);
358     },
359
360     rightSidebarResizerDrag: function(event)
361     {
362         var rightSidebar = this.views.dom.sidebarElement;
363         if (rightSidebar.dragging == true) {
364             var x = event.clientX + window.scrollX;
365
366             var leftSidebarWidth = window.getComputedStyle(document.getElementById("sidebar")).getPropertyCSSValue("width").getFloatValue(CSSPrimitiveValue.CSS_PX);
367             var newWidth = Number.constrain(window.innerWidth - x, 100, window.innerWidth - leftSidebarWidth - 100);
368
369             if (x == newWidth)
370                 rightSidebar.dragLastX = x;
371
372             rightSidebar.style.width = newWidth + "px";
373             this.views.dom.sideContentElement.style.right = newWidth + "px";
374             this.views.dom.sidebarResizeElement.style.right = (newWidth - 3) + "px";
375
376             this.updateTreeSelection();
377
378             event.preventDefault();
379         }
380     }
381 }
382
383 WebInspector.DocumentPanel.prototype.__proto__ = WebInspector.SourcePanel.prototype;
384
385 WebInspector.DOMNodeTreeElement = function(node)
386 {
387     var hasChildren = (Preferences.ignoreWhitespace ? (firstChildSkippingWhitespace.call(node) ? true : false) : node.hasChildNodes());
388     var titleInfo = nodeTitleInfo.call(node, hasChildren, WebInspector.linkifyURL);
389
390     if (titleInfo.hasChildren) 
391         this.whitespaceIgnored = Preferences.ignoreWhitespace;
392
393     TreeElement.call(this, titleInfo.title, node, titleInfo.hasChildren);
394 }
395
396 WebInspector.DOMNodeTreeElement.prototype = {
397     updateSelection: function()
398     {
399         if (!this._listItemNode)
400             return;
401
402         if (!this.selectionElement) {
403             this.selectionElement = document.createElement("div");
404             this.selectionElement.className = "selection selected";
405             this._listItemNode.insertBefore(this.selectionElement, this._listItemNode.firstChild);
406         }
407
408         this.selectionElement.style.height = this._listItemNode.offsetHeight + "px";
409     },
410
411     onpopulate: function()
412     {
413         if (this.children.length || this.whitespaceIgnored !== Preferences.ignoreWhitespace)
414             return;
415
416         this.removeChildren();
417         this.whitespaceIgnored = Preferences.ignoreWhitespace;
418
419         var node = (Preferences.ignoreWhitespace ? firstChildSkippingWhitespace.call(this.representedObject) : this.representedObject.firstChild);
420         while (node) {
421             this.appendChild(new WebInspector.DOMNodeTreeElement(node));
422             node = Preferences.ignoreWhitespace ? nextSiblingSkippingWhitespace.call(node) : node.nextSibling;
423         }
424
425         if (this.representedObject.nodeType == Node.ELEMENT_NODE) {
426             var title = "<span class=\"webkit-html-tag close\">&lt;/" + this.representedObject.nodeName.toLowerCase().escapeHTML() + "&gt;</span>";
427             var item = new TreeElement(title, this.representedObject, false);
428             item.selectable = false;
429             this.appendChild(item);
430         }
431     },
432
433     onexpand: function()
434     {
435         this.treeOutline.panel.updateTreeSelection();
436     },
437
438     oncollapse: function()
439     {
440         this.treeOutline.panel.updateTreeSelection();
441     },
442
443     onreveal: function()
444     {
445         if (!this._listItemNode || !this.treeOutline || !this.treeOutline._childrenListNode)
446             return;
447         var parent = this.treeOutline.panel.views.dom.treeContentElement;
448         parent.scrollToElement(this._listItemNode);
449     },
450
451     onselect: function()
452     {
453         this.treeOutline.panel.focusedDOMNode = this.representedObject;
454
455         // Call updateSelection twice to make sure the height is correct,
456         // the first time might have a bad height because we are in a weird tree state
457         this.updateSelection();
458
459         var element = this;
460         setTimeout(function() { element.updateSelection() }, 0);
461     },
462
463     ondblclick: function()
464     {
465         var panel = this.treeOutline.panel;
466         panel.rootDOMNode = this.representedObject.parentNode;
467         panel.focusedDOMNode = this.representedObject;
468     }
469 }
470
471 WebInspector.DOMNodeTreeElement.prototype.__proto__ = TreeElement.prototype;