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