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