47105bc66f5b96276c3e149dc706ee6dd23a04cf
[WebKit-https.git] / WebCore / page / inspector / ElementsPanel.js
1 /*
2  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3  * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1.  Redistributions of source code must retain the above copyright
10  *     notice, this list of conditions and the following disclaimer.
11  * 2.  Redistributions in binary form must reproduce the above copyright
12  *     notice, this list of conditions and the following disclaimer in the
13  *     documentation and/or other materials provided with the distribution.
14  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
15  *     its contributors may be used to endorse or promote products derived
16  *     from this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
19  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
22  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 WebInspector.ElementsPanel = function()
31 {
32     WebInspector.Panel.call(this);
33
34     this.element.addStyleClass("elements");
35
36     this.contentElement = document.createElement("div");
37     this.contentElement.id = "elements-content";
38     this.contentElement.className = "outline-disclosure";
39
40     this.treeOutline = new WebInspector.ElementsTreeOutline();
41     this.treeOutline.panel = this;
42     this.treeOutline.includeRootDOMNode = false;
43     this.treeOutline.selectEnabled = true;
44
45     this.treeOutline.focusedNodeChanged = function(forceUpdate)
46     {
47         this.panel.updateBreadcrumb(forceUpdate);
48
49         for (var pane in this.panel.sidebarPanes)
50            this.panel.sidebarPanes[pane].needsUpdate = true;
51
52         this.panel.updateStyles(true);
53         this.panel.updateMetrics();
54         this.panel.updateProperties();
55
56         if (InspectorController.searchingForNode()) {
57             InspectorController.toggleNodeSearch();
58             this.panel.nodeSearchButton.removeStyleClass("toggled-on");
59         }
60     };
61
62     this.contentElement.appendChild(this.treeOutline.element);
63
64     this.crumbsElement = document.createElement("div");
65     this.crumbsElement.className = "crumbs";
66     this.crumbsElement.addEventListener("mousemove", this._mouseMovedInCrumbs.bind(this), false);
67     this.crumbsElement.addEventListener("mouseout", this._mouseMovedOutOfCrumbs.bind(this), false);
68
69     this.sidebarPanes = {};
70     this.sidebarPanes.styles = new WebInspector.StylesSidebarPane();
71     this.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
72     this.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
73
74     this.sidebarPanes.styles.onexpand = this.updateStyles.bind(this);
75     this.sidebarPanes.metrics.onexpand = this.updateMetrics.bind(this);
76     this.sidebarPanes.properties.onexpand = this.updateProperties.bind(this);
77
78     this.sidebarPanes.styles.expanded = true;
79
80     this.sidebarPanes.styles.addEventListener("style edited", this._stylesPaneEdited, this);
81     this.sidebarPanes.styles.addEventListener("style property toggled", this._stylesPaneEdited, this);
82     this.sidebarPanes.metrics.addEventListener("metrics edited", this._metricsPaneEdited, this);
83
84     this.sidebarElement = document.createElement("div");
85     this.sidebarElement.id = "elements-sidebar";
86
87     this.sidebarElement.appendChild(this.sidebarPanes.styles.element);
88     this.sidebarElement.appendChild(this.sidebarPanes.metrics.element);
89     this.sidebarElement.appendChild(this.sidebarPanes.properties.element);
90
91     this.sidebarResizeElement = document.createElement("div");
92     this.sidebarResizeElement.className = "sidebar-resizer-vertical";
93     this.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false);
94
95     this.nodeSearchButton = document.createElement("button");
96     this.nodeSearchButton.title = WebInspector.UIString("Select an element in the page to inspect it.");
97     this.nodeSearchButton.id = "node-search-status-bar-item";
98     this.nodeSearchButton.className = "status-bar-item";
99     this.nodeSearchButton.addEventListener("click", this._nodeSearchButtonClicked.bind(this), false);
100
101     this.searchingForNode = false;
102
103     this.element.appendChild(this.contentElement);
104     this.element.appendChild(this.sidebarElement);
105     this.element.appendChild(this.sidebarResizeElement);
106
107     this._mutationMonitoredWindows = [];
108     this._nodeInsertedEventListener = InspectorController.wrapCallback(this._nodeInserted.bind(this));
109     this._nodeRemovedEventListener = InspectorController.wrapCallback(this._nodeRemoved.bind(this));
110     this._contentLoadedEventListener = InspectorController.wrapCallback(this._contentLoaded.bind(this));
111
112     this.reset();
113 }
114
115 WebInspector.ElementsPanel.prototype = {
116     toolbarItemClass: "elements",
117
118     get toolbarItemLabel()
119     {
120         return WebInspector.UIString("Elements");
121     },
122
123     get statusBarItems()
124     {
125         return [this.nodeSearchButton, this.crumbsElement];
126     },
127
128     updateStatusBarItems: function()
129     {
130         this.updateBreadcrumbSizes();
131     },
132
133     show: function()
134     {
135         WebInspector.Panel.prototype.show.call(this);
136         this.sidebarResizeElement.style.right = (this.sidebarElement.offsetWidth - 3) + "px";
137         this.updateBreadcrumb();
138         this.treeOutline.updateSelection();
139         if (this.recentlyModifiedNodes.length)
140             this._updateModifiedNodes();
141     },
142
143     hide: function()
144     {
145         WebInspector.Panel.prototype.hide.call(this);
146
147         WebInspector.hoveredDOMNode = null;
148
149         if (InspectorController.searchingForNode()) {
150             InspectorController.toggleNodeSearch();
151             this.nodeSearchButton.removeStyleClass("toggled-on");
152         }
153     },
154
155     resize: function()
156     {
157         this.treeOutline.updateSelection();
158         this.updateBreadcrumbSizes();
159     },
160
161     reset: function()
162     {
163         this.rootDOMNode = null;
164         this.focusedDOMNode = null;
165
166         WebInspector.hoveredDOMNode = null;
167
168         if (InspectorController.searchingForNode()) {
169             InspectorController.toggleNodeSearch();
170             this.nodeSearchButton.removeStyleClass("toggled-on");
171         }
172
173         this.recentlyModifiedNodes = [];
174         this.unregisterAllMutationEventListeners();
175
176         delete this.currentQuery;
177         this.searchCanceled();
178
179         var inspectedWindow = InspectorController.inspectedWindow();
180         if (!inspectedWindow || !inspectedWindow.document)
181             return;
182
183         if (!inspectedWindow.document.firstChild) {
184             function contentLoaded()
185             {
186                 inspectedWindow.document.removeEventListener("DOMContentLoaded", contentLoadedCallback, false);
187
188                 this.reset();
189             }
190
191             var contentLoadedCallback = InspectorController.wrapCallback(contentLoaded.bind(this));
192             inspectedWindow.document.addEventListener("DOMContentLoaded", contentLoadedCallback, false);
193             return;
194         }
195
196         // If the window isn't visible, return early so the DOM tree isn't built
197         // and mutation event listeners are not added.
198         if (!InspectorController.isWindowVisible())
199             return;
200
201         this.registerMutationEventListeners(inspectedWindow);
202
203         var inspectedRootDocument = inspectedWindow.document;
204         this.rootDOMNode = inspectedRootDocument;
205
206         var canidateFocusNode = inspectedRootDocument.body || inspectedRootDocument.documentElement;
207         if (canidateFocusNode) {
208             this.treeOutline.suppressSelectHighlight = true;
209             this.focusedDOMNode = canidateFocusNode;
210             this.treeOutline.suppressSelectHighlight = false;
211
212             if (this.treeOutline.selectedTreeElement)
213                 this.treeOutline.selectedTreeElement.expand();
214         }
215     },
216
217     searchCanceled: function()
218     {
219         if (this._searchResults) {
220             for (var i = 0; i < this._searchResults.length; ++i) {
221                 var node = this._searchResults[i];
222                 var treeElement = this.treeOutline.findTreeElement(node);
223                 if (treeElement)
224                     treeElement.highlighted = false;
225             }
226         }
227
228         WebInspector.updateSearchMatchesCount(0, this);
229
230         this._currentSearchResultIndex = 0;
231         this._searchResults = [];
232     },
233
234     performSearch: function(query)
235     {
236         // Call searchCanceled since it will reset everything we need before doing a new search.
237         this.searchCanceled();
238
239         var escapedQuery = query.escapeCharacters("'");
240         var plainTextXPathQuery = "//*[contains(name(),'" + escapedQuery + "') or contains(@*,'" + escapedQuery + "')] | //text()[contains(.,'" + escapedQuery + "')] | //comment()[contains(.,'" + escapedQuery + "')]";
241
242         var evaluateFunction = InspectorController.inspectedWindow().Document.prototype.evaluate;
243         var querySelectorAllFunction = InspectorController.inspectedWindow().Document.prototype.querySelectorAll;
244
245         const searchResultsProperty = "__includedInInspectorSearchResults";
246         function addNodesToResults(nodes, length, getItem)
247         {
248             for (var i = 0; i < length; ++i) {
249                 var node = getItem(nodes, i);
250                 // Skip this node if it already has the property.
251                 if (searchResultsProperty in node)
252                     continue;
253                 node[searchResultsProperty] = true;
254                 this._searchResults.push(node);
255             }
256         }
257
258         function searchDocument(doc)
259         {
260             try {
261                 var result = evaluateFunction.call(doc, plainTextXPathQuery, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
262                 addNodesToResults.call(this, result, result.snapshotLength, function(l, i) { return l.snapshotItem(i); });
263             } catch(err) {
264                 // ignore any exceptions. the query might be malformed, but we allow that.
265             }
266
267             try {
268                 var result = evaluateFunction.call(doc, query, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
269                 addNodesToResults.call(this, result, result.snapshotLength, function(l, i) { return l.snapshotItem(i); });
270             } catch(err) {
271                 // ignore any exceptions. the query might be malformed, but we allow that.
272             }
273
274             try {
275                 var result = querySelectorAllFunction.call(doc, query);
276                 addNodesToResults.call(this, result, result.length, function(l, i) { return l.item(i); });
277             } catch(err) {
278                 // ignore any exceptions. the query isn't necessarily a valid selector.
279             }
280
281             // Remove the searchResultsProperty now that the search is finished.
282             for (var i = 0; i < this._searchResults.length; ++i)
283                 delete this._searchResults[i][searchResultsProperty];
284         }
285
286         var mainFrameDocument = InspectorController.inspectedWindow().document;
287
288         searchDocument.call(this, mainFrameDocument);
289
290         // Find all frames, iframes and object elements to search their documents.
291         var subdocumentQuery = "//iframe | //frame | //object";
292         var subdocumentResult = evaluateFunction.call(mainFrameDocument, subdocumentQuery, mainFrameDocument, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
293
294         for (var i = 0; i < subdocumentResult.snapshotLength; ++i) {
295             var element = subdocumentResult.snapshotItem(i);
296             if (element.contentDocument)
297                 searchDocument.call(this, element.contentDocument);
298         }
299
300         if (!this._searchResults.length)
301             return;
302
303         for (var i = 0; i < this._searchResults.length; ++i) {
304             var node = this._searchResults[i];
305             var treeElement = this.treeOutline.findTreeElement(node);
306             if (treeElement)
307                 treeElement.highlighted = true;
308         }
309
310         WebInspector.updateSearchMatchesCount(this._searchResults.length, this);
311
312         this._currentSearchResultIndex = 0;
313         this.focusedDOMNode = this._searchResults[0];
314     },
315
316     jumpToNextSearchResult: function()
317     {
318         if (!this._searchResults || !this._searchResults.length)
319             return;
320         if (++this._currentSearchResultIndex >= this._searchResults.length)
321             this._currentSearchResultIndex = 0;
322         this.focusedDOMNode = this._searchResults[this._currentSearchResultIndex];
323     },
324
325     jumpToPreviousSearchResult: function()
326     {
327         if (!this._searchResults || !this._searchResults.length)
328             return;
329         if (--this._currentSearchResultIndex < 0)
330             this._currentSearchResultIndex = (this._searchResults.length - 1);
331         this.focusedDOMNode = this._searchResults[this._currentSearchResultIndex];
332     },
333
334     inspectedWindowCleared: function(window)
335     {
336         if (InspectorController.isWindowVisible())
337             this.updateMutationEventListeners(window);
338     },
339
340     _addMutationEventListeners: function(monitoredWindow)
341     {
342         monitoredWindow.document.addEventListener("DOMNodeInserted", this._nodeInsertedEventListener, true);
343         monitoredWindow.document.addEventListener("DOMNodeRemoved", this._nodeRemovedEventListener, true);
344         if (monitoredWindow.frameElement)
345             monitoredWindow.addEventListener("DOMContentLoaded", this._contentLoadedEventListener, true);
346     },
347
348     _removeMutationEventListeners: function(monitoredWindow)
349     {
350         if (monitoredWindow.frameElement)
351             monitoredWindow.removeEventListener("DOMContentLoaded", this._contentLoadedEventListener, true);
352         if (!monitoredWindow.document)
353             return;
354         monitoredWindow.document.removeEventListener("DOMNodeInserted", this._nodeInsertedEventListener, true);
355         monitoredWindow.document.removeEventListener("DOMNodeRemoved", this._nodeRemovedEventListener, true);
356     },
357
358     updateMutationEventListeners: function(monitoredWindow)
359     {
360         this._addMutationEventListeners(monitoredWindow);
361     },
362
363     registerMutationEventListeners: function(monitoredWindow)
364     {
365         if (!monitoredWindow || this._mutationMonitoredWindows.indexOf(monitoredWindow) !== -1)
366             return;
367         this._mutationMonitoredWindows.push(monitoredWindow);
368         if (InspectorController.isWindowVisible())
369             this._addMutationEventListeners(monitoredWindow);
370     },
371
372     unregisterMutationEventListeners: function(monitoredWindow)
373     {
374         if (!monitoredWindow || this._mutationMonitoredWindows.indexOf(monitoredWindow) === -1)
375             return;
376         this._mutationMonitoredWindows.remove(monitoredWindow);
377         this._removeMutationEventListeners(monitoredWindow);
378     },
379
380     unregisterAllMutationEventListeners: function()
381     {
382         for (var i = 0; i < this._mutationMonitoredWindows.length; ++i)
383             this._removeMutationEventListeners(this._mutationMonitoredWindows[i]);
384         this._mutationMonitoredWindows = [];
385     },
386
387     get rootDOMNode()
388     {
389         return this.treeOutline.rootDOMNode;
390     },
391
392     set rootDOMNode(x)
393     {
394         this.treeOutline.rootDOMNode = x;
395     },
396
397     get focusedDOMNode()
398     {
399         return this.treeOutline.focusedDOMNode;
400     },
401
402     set focusedDOMNode(x)
403     {
404         this.treeOutline.focusedDOMNode = x;
405     },
406
407     _contentLoaded: function(event)
408     {
409         this.recentlyModifiedNodes.push({node: event.target, parent: event.target.defaultView.frameElement, replaced: true});
410         if (this.visible)
411             this._updateModifiedNodesSoon();
412     },
413
414     _nodeInserted: function(event)
415     {
416         this.recentlyModifiedNodes.push({node: event.target, parent: event.relatedNode, inserted: true});
417         if (this.visible)
418             this._updateModifiedNodesSoon();
419     },
420
421     _nodeRemoved: function(event)
422     {
423         this.recentlyModifiedNodes.push({node: event.target, parent: event.relatedNode, removed: true});
424         if (this.visible)
425             this._updateModifiedNodesSoon();
426     },
427
428     _updateModifiedNodesSoon: function()
429     {
430         if ("_updateModifiedNodesTimeout" in this)
431             return;
432         this._updateModifiedNodesTimeout = setTimeout(this._updateModifiedNodes.bind(this), 0);
433     },
434
435     _updateModifiedNodes: function()
436     {
437         if ("_updateModifiedNodesTimeout" in this) {
438             clearTimeout(this._updateModifiedNodesTimeout);
439             delete this._updateModifiedNodesTimeout;
440         }
441
442         var updatedParentTreeElements = [];
443         var updateBreadcrumbs = false;
444
445         for (var i = 0; i < this.recentlyModifiedNodes.length; ++i) {
446             var replaced = this.recentlyModifiedNodes[i].replaced;
447             var parent = this.recentlyModifiedNodes[i].parent;
448             if (!parent)
449                 continue;
450
451             var parentNodeItem = this.treeOutline.findTreeElement(parent, null, null, objectsAreSame);
452             if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) {
453                 parentNodeItem.updateChildren(replaced);
454                 parentNodeItem.alreadyUpdatedChildren = true;
455                 updatedParentTreeElements.push(parentNodeItem);
456             }
457
458             if (!updateBreadcrumbs && (objectsAreSame(this.focusedDOMNode, parent) || isAncestorIncludingParentFrames(this.focusedDOMNode, parent)))
459                 updateBreadcrumbs = true;
460         }
461
462         for (var i = 0; i < updatedParentTreeElements.length; ++i)
463             delete updatedParentTreeElements[i].alreadyUpdatedChildren;
464
465         this.recentlyModifiedNodes = [];
466
467         if (updateBreadcrumbs)
468             this.updateBreadcrumb(true);
469     },
470
471     _stylesPaneEdited: function()
472     {
473         this.sidebarPanes.metrics.needsUpdate = true;
474         this.updateMetrics();
475     },
476
477     _metricsPaneEdited: function()
478     {
479         this.sidebarPanes.styles.needsUpdate = true;
480         this.updateStyles(true);
481     },
482
483     _mouseMovedInCrumbs: function(event)
484     {
485         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
486         var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb");
487
488         WebInspector.hoveredDOMNode = (crumbElement ? crumbElement.representedObject : null);
489
490         if ("_mouseOutOfCrumbsTimeout" in this) {
491             clearTimeout(this._mouseOutOfCrumbsTimeout);
492             delete this._mouseOutOfCrumbsTimeout;
493         }
494     },
495
496     _mouseMovedOutOfCrumbs: function(event)
497     {
498         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
499         if (nodeUnderMouse.isDescendant(this.crumbsElement))
500             return;
501
502         WebInspector.hoveredDOMNode = null;
503
504         this._mouseOutOfCrumbsTimeout = setTimeout(this.updateBreadcrumbSizes.bind(this), 1000);
505     },
506
507     updateBreadcrumb: function(forceUpdate)
508     {
509         if (!this.visible)
510             return;
511
512         var crumbs = this.crumbsElement;
513
514         var handled = false;
515         var foundRoot = false;
516         var crumb = crumbs.firstChild;
517         while (crumb) {
518             if (objectsAreSame(crumb.representedObject, this.rootDOMNode))
519                 foundRoot = true;
520
521             if (foundRoot)
522                 crumb.addStyleClass("dimmed");
523             else
524                 crumb.removeStyleClass("dimmed");
525
526             if (objectsAreSame(crumb.representedObject, this.focusedDOMNode)) {
527                 crumb.addStyleClass("selected");
528                 handled = true;
529             } else {
530                 crumb.removeStyleClass("selected");
531             }
532
533             crumb = crumb.nextSibling;
534         }
535
536         if (handled && !forceUpdate) {
537             // We don't need to rebuild the crumbs, but we need to adjust sizes
538             // to reflect the new focused or root node.
539             this.updateBreadcrumbSizes();
540             return;
541         }
542
543         crumbs.removeChildren();
544
545         var panel = this;
546
547         function selectCrumbFunction(event)
548         {
549             var crumb = event.currentTarget;
550             if (crumb.hasStyleClass("collapsed")) {
551                 // Clicking a collapsed crumb will expose the hidden crumbs.
552                 if (crumb === panel.crumbsElement.firstChild) {
553                     // If the focused crumb is the first child, pick the farthest crumb
554                     // that is still hidden. This allows the user to expose every crumb.
555                     var currentCrumb = crumb;
556                     while (currentCrumb) {
557                         var hidden = currentCrumb.hasStyleClass("hidden");
558                         var collapsed = currentCrumb.hasStyleClass("collapsed");
559                         if (!hidden && !collapsed)
560                             break;
561                         crumb = currentCrumb;
562                         currentCrumb = currentCrumb.nextSibling;
563                     }
564                 }
565
566                 panel.updateBreadcrumbSizes(crumb);
567             } else {
568                 // Clicking a dimmed crumb or double clicking (event.detail >= 2)
569                 // will change the root node in addition to the focused node.
570                 if (event.detail >= 2 || crumb.hasStyleClass("dimmed"))
571                     panel.rootDOMNode = crumb.representedObject.parentNode;
572                 panel.focusedDOMNode = crumb.representedObject;
573             }
574
575             WebInspector.currentFocusElement = document.getElementById("main-panels");
576
577             event.preventDefault();
578         }
579
580         foundRoot = false;
581         for (var current = this.focusedDOMNode; current; current = parentNodeOrFrameElement(current)) {
582             if (current.nodeType === Node.DOCUMENT_NODE)
583                 continue;
584
585             if (objectsAreSame(current, this.rootDOMNode))
586                 foundRoot = true;
587
588             var crumb = document.createElement("span");
589             crumb.className = "crumb";
590             crumb.representedObject = current;
591             crumb.addEventListener("mousedown", selectCrumbFunction, false);
592
593             var crumbTitle;
594             switch (current.nodeType) {
595                 case Node.ELEMENT_NODE:
596                     crumbTitle = current.nodeName.toLowerCase();
597
598                     var nameElement = document.createElement("span");
599                     nameElement.textContent = crumbTitle;
600                     crumb.appendChild(nameElement);
601
602                     var idAttribute = current.getAttribute("id");
603                     if (idAttribute) {
604                         var idElement = document.createElement("span");
605                         crumb.appendChild(idElement);
606
607                         var part = "#" + idAttribute;
608                         crumbTitle += part;
609                         idElement.appendChild(document.createTextNode(part));
610
611                         // Mark the name as extra, since the ID is more important.
612                         nameElement.className = "extra";
613                     }
614
615                     var classAttribute = current.getAttribute("class");
616                     if (classAttribute) {
617                         var classes = classAttribute.split(/\s+/);
618                         var foundClasses = {};
619
620                         if (classes.length) {
621                             var classesElement = document.createElement("span");
622                             classesElement.className = "extra";
623                             crumb.appendChild(classesElement);
624
625                             for (var i = 0; i < classes.length; ++i) {
626                                 var className = classes[i];
627                                 if (className && !(className in foundClasses)) {
628                                     var part = "." + className;
629                                     crumbTitle += part;
630                                     classesElement.appendChild(document.createTextNode(part));
631                                     foundClasses[className] = true;
632                                 }
633                             }
634                         }
635                     }
636
637                     break;
638
639                 case Node.TEXT_NODE:
640                     if (isNodeWhitespace.call(current))
641                         crumbTitle = WebInspector.UIString("(whitespace)");
642                     else
643                         crumbTitle = WebInspector.UIString("(text)");
644                     break
645
646                 case Node.COMMENT_NODE:
647                     crumbTitle = "<!-->";
648                     break;
649
650                 case Node.DOCUMENT_TYPE_NODE:
651                     crumbTitle = "<!DOCTYPE>";
652                     break;
653
654                 default:
655                     crumbTitle = current.nodeName.toLowerCase();
656             }
657
658             if (!crumb.childNodes.length) {
659                 var nameElement = document.createElement("span");
660                 nameElement.textContent = crumbTitle;
661                 crumb.appendChild(nameElement);
662             }
663
664             crumb.title = crumbTitle;
665
666             if (foundRoot)
667                 crumb.addStyleClass("dimmed");
668             if (objectsAreSame(current, this.focusedDOMNode))
669                 crumb.addStyleClass("selected");
670             if (!crumbs.childNodes.length)
671                 crumb.addStyleClass("end");
672
673             crumbs.appendChild(crumb);
674         }
675
676         if (crumbs.hasChildNodes())
677             crumbs.lastChild.addStyleClass("start");
678
679         this.updateBreadcrumbSizes();
680     },
681
682     updateBreadcrumbSizes: function(focusedCrumb)
683     {
684         if (!this.visible)
685             return;
686
687         if (document.body.offsetWidth <= 0) {
688             // The stylesheet hasn't loaded yet or the window is closed,
689             // so we can't calculate what is need. Return early.
690             return;
691         }
692
693         var crumbs = this.crumbsElement;
694         if (!crumbs.childNodes.length || crumbs.offsetWidth <= 0)
695             return; // No crumbs, do nothing.
696
697         // A Zero index is the right most child crumb in the breadcrumb.
698         var selectedIndex = 0;
699         var focusedIndex = 0;
700         var selectedCrumb;
701
702         var i = 0;
703         var crumb = crumbs.firstChild;
704         while (crumb) {
705             // Find the selected crumb and index. 
706             if (!selectedCrumb && crumb.hasStyleClass("selected")) {
707                 selectedCrumb = crumb;
708                 selectedIndex = i;
709             }
710
711             // Find the focused crumb index. 
712             if (crumb === focusedCrumb)
713                 focusedIndex = i;
714
715             // Remove any styles that affect size before
716             // deciding to shorten any crumbs.
717             if (crumb !== crumbs.lastChild)
718                 crumb.removeStyleClass("start");
719             if (crumb !== crumbs.firstChild)
720                 crumb.removeStyleClass("end");
721
722             crumb.removeStyleClass("compact");
723             crumb.removeStyleClass("collapsed");
724             crumb.removeStyleClass("hidden");
725
726             crumb = crumb.nextSibling;
727             ++i;
728         }
729
730         // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs().
731         // The order of the crumbs in the document is opposite of the visual order.
732         crumbs.firstChild.addStyleClass("end");
733         crumbs.lastChild.addStyleClass("start");
734
735         function crumbsAreSmallerThanContainer()
736         {
737             var rightPadding = 20;
738             var errorWarningElement = document.getElementById("error-warning-count");
739             if (!WebInspector.console.visible && errorWarningElement)
740                 rightPadding += errorWarningElement.offsetWidth;
741             return ((crumbs.totalOffsetLeft + crumbs.offsetWidth + rightPadding) < window.innerWidth);
742         }
743
744         if (crumbsAreSmallerThanContainer())
745             return; // No need to compact the crumbs, they all fit at full size.
746
747         var BothSides = 0;
748         var AncestorSide = -1;
749         var ChildSide = 1;
750
751         function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb)
752         {
753             if (!significantCrumb)
754                 significantCrumb = (focusedCrumb || selectedCrumb);
755
756             if (significantCrumb === selectedCrumb)
757                 var significantIndex = selectedIndex;
758             else if (significantCrumb === focusedCrumb)
759                 var significantIndex = focusedIndex;
760             else {
761                 var significantIndex = 0;
762                 for (var i = 0; i < crumbs.childNodes.length; ++i) {
763                     if (crumbs.childNodes[i] === significantCrumb) {
764                         significantIndex = i;
765                         break;
766                     }
767                 }
768             }
769
770             function shrinkCrumbAtIndex(index)
771             {
772                 var shrinkCrumb = crumbs.childNodes[index];
773                 if (shrinkCrumb && shrinkCrumb !== significantCrumb)
774                     shrinkingFunction(shrinkCrumb);
775                 if (crumbsAreSmallerThanContainer())
776                     return true; // No need to compact the crumbs more.
777                 return false;
778             }
779
780             // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
781             // fit in the container or we run out of crumbs to shrink.
782             if (direction) {
783                 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
784                 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
785                 while (index !== significantIndex) {
786                     if (shrinkCrumbAtIndex(index))
787                         return true;
788                     index += (direction > 0 ? 1 : -1);
789                 }
790             } else {
791                 // Crumbs are shrunk in order of descending distance from the signifcant crumb,
792                 // with a tie going to child crumbs.
793                 var startIndex = 0;
794                 var endIndex = crumbs.childNodes.length - 1;
795                 while (startIndex != significantIndex || endIndex != significantIndex) {
796                     var startDistance = significantIndex - startIndex;
797                     var endDistance = endIndex - significantIndex;
798                     if (startDistance >= endDistance)
799                         var index = startIndex++;
800                     else
801                         var index = endIndex--;
802                     if (shrinkCrumbAtIndex(index))
803                         return true;
804                 }
805             }
806
807             // We are not small enough yet, return false so the caller knows.
808             return false;
809         }
810
811         function coalesceCollapsedCrumbs()
812         {
813             var crumb = crumbs.firstChild;
814             var collapsedRun = false;
815             var newStartNeeded = false;
816             var newEndNeeded = false;
817             while (crumb) {
818                 var hidden = crumb.hasStyleClass("hidden");
819                 if (!hidden) {
820                     var collapsed = crumb.hasStyleClass("collapsed"); 
821                     if (collapsedRun && collapsed) {
822                         crumb.addStyleClass("hidden");
823                         crumb.removeStyleClass("compact");
824                         crumb.removeStyleClass("collapsed");
825
826                         if (crumb.hasStyleClass("start")) {
827                             crumb.removeStyleClass("start");
828                             newStartNeeded = true;
829                         }
830
831                         if (crumb.hasStyleClass("end")) {
832                             crumb.removeStyleClass("end");
833                             newEndNeeded = true;
834                         }
835
836                         continue;
837                     }
838
839                     collapsedRun = collapsed;
840
841                     if (newEndNeeded) {
842                         newEndNeeded = false;
843                         crumb.addStyleClass("end");
844                     }
845                 } else
846                     collapsedRun = true;
847                 crumb = crumb.nextSibling;
848             }
849
850             if (newStartNeeded) {
851                 crumb = crumbs.lastChild;
852                 while (crumb) {
853                     if (!crumb.hasStyleClass("hidden")) {
854                         crumb.addStyleClass("start");
855                         break;
856                     }
857                     crumb = crumb.previousSibling;
858                 }
859             }
860         }
861
862         function compact(crumb)
863         {
864             if (crumb.hasStyleClass("hidden"))
865                 return;
866             crumb.addStyleClass("compact");
867         }
868
869         function collapse(crumb, dontCoalesce)
870         {
871             if (crumb.hasStyleClass("hidden"))
872                 return;
873             crumb.addStyleClass("collapsed");
874             crumb.removeStyleClass("compact");
875             if (!dontCoalesce)
876                 coalesceCollapsedCrumbs();
877         }
878
879         function compactDimmed(crumb)
880         {
881             if (crumb.hasStyleClass("dimmed"))
882                 compact(crumb);
883         }
884
885         function collapseDimmed(crumb)
886         {
887             if (crumb.hasStyleClass("dimmed"))
888                 collapse(crumb);
889         }
890
891         if (!focusedCrumb) {
892             // When not focused on a crumb we can be biased and collapse less important
893             // crumbs that the user might not care much about.
894
895             // Compact child crumbs.
896             if (makeCrumbsSmaller(compact, ChildSide))
897                 return;
898
899             // Collapse child crumbs.
900             if (makeCrumbsSmaller(collapse, ChildSide))
901                 return;
902
903             // Compact dimmed ancestor crumbs.
904             if (makeCrumbsSmaller(compactDimmed, AncestorSide))
905                 return;
906
907             // Collapse dimmed ancestor crumbs.
908             if (makeCrumbsSmaller(collapseDimmed, AncestorSide))
909                 return;
910         }
911
912         // Compact ancestor crumbs, or from both sides if focused.
913         if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide)))
914             return;
915
916         // Collapse ancestor crumbs, or from both sides if focused.
917         if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide)))
918             return;
919
920         if (!selectedCrumb)
921             return;
922
923         // Compact the selected crumb.
924         compact(selectedCrumb);
925         if (crumbsAreSmallerThanContainer())
926             return;
927
928         // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
929         collapse(selectedCrumb, true);
930     },
931
932     updateStyles: function(forceUpdate)
933     {
934         var stylesSidebarPane = this.sidebarPanes.styles;
935         if (!stylesSidebarPane.expanded || !stylesSidebarPane.needsUpdate)
936             return;
937
938         stylesSidebarPane.update(this.focusedDOMNode, null, forceUpdate);
939         stylesSidebarPane.needsUpdate = false;
940     },
941
942     updateMetrics: function()
943     {
944         var metricsSidebarPane = this.sidebarPanes.metrics;
945         if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate)
946             return;
947
948         metricsSidebarPane.update(this.focusedDOMNode);
949         metricsSidebarPane.needsUpdate = false;
950     },
951
952     updateProperties: function()
953     {
954         var propertiesSidebarPane = this.sidebarPanes.properties;
955         if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate)
956             return;
957
958         propertiesSidebarPane.update(this.focusedDOMNode);
959         propertiesSidebarPane.needsUpdate = false;
960     },
961
962     handleKeyEvent: function(event)
963     {
964         this.treeOutline.handleKeyEvent(event);
965     },
966
967     handleCopyEvent: function(event)
968     {
969         // Don't prevent the normal copy if the user has a selection.
970         if (!window.getSelection().isCollapsed)
971             return;
972
973         switch (this.focusedDOMNode.nodeType) {
974             case Node.ELEMENT_NODE:
975                 var data = this.focusedDOMNode.outerHTML;
976                 break;
977
978             case Node.COMMENT_NODE:
979                 var data = "<!--" + this.focusedDOMNode.nodeValue + "-->";
980                 break;
981
982             default:
983             case Node.TEXT_NODE:
984                 var data = this.focusedDOMNode.nodeValue;
985         }
986
987         event.clipboardData.clearData();
988         event.preventDefault();
989
990         if (data)
991             event.clipboardData.setData("text/plain", data);
992     },
993
994     rightSidebarResizerDragStart: function(event)
995     {
996         WebInspector.elementDragStart(this.sidebarElement, this.rightSidebarResizerDrag.bind(this), this.rightSidebarResizerDragEnd.bind(this), event, "col-resize");
997     },
998
999     rightSidebarResizerDragEnd: function(event)
1000     {
1001         WebInspector.elementDragEnd(event);
1002     },
1003
1004     rightSidebarResizerDrag: function(event)
1005     {
1006         var x = event.pageX;
1007         var newWidth = Number.constrain(window.innerWidth - x, Preferences.minElementsSidebarWidth, window.innerWidth * 0.66);
1008
1009         this.sidebarElement.style.width = newWidth + "px";
1010         this.contentElement.style.right = newWidth + "px";
1011         this.sidebarResizeElement.style.right = (newWidth - 3) + "px";
1012
1013         this.treeOutline.updateSelection();
1014
1015         event.preventDefault();
1016     },
1017
1018     _nodeSearchButtonClicked: function(event)
1019     {
1020         InspectorController.toggleNodeSearch();
1021
1022         if (InspectorController.searchingForNode())
1023             this.nodeSearchButton.addStyleClass("toggled-on");
1024         else
1025             this.nodeSearchButton.removeStyleClass("toggled-on");
1026     }
1027 }
1028
1029 WebInspector.ElementsPanel.prototype.__proto__ = WebInspector.Panel.prototype;