Web Inspector: Split out crumb list part of styles from elementsPanel.css
[WebKit-https.git] / Source / WebCore / inspector / front-end / ElementsPanel.js
1 /*
2  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
3  * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
4  * Copyright (C) 2009 Joseph Pecoraro
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
10  * 1.  Redistributions of source code must retain the above copyright
11  *     notice, this list of conditions and the following disclaimer.
12  * 2.  Redistributions in binary form must reproduce the above copyright
13  *     notice, this list of conditions and the following disclaimer in the
14  *     documentation and/or other materials provided with the distribution.
15  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16  *     its contributors may be used to endorse or promote products derived
17  *     from this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 importScript("EventListenersSidebarPane.js");
32 importScript("MetricsSidebarPane.js");
33 importScript("PropertiesSidebarPane.js");
34 importScript("StylesSidebarPane.js");
35
36 /**
37  * @constructor
38  * @extends {WebInspector.Panel}
39  */
40 WebInspector.ElementsPanel = function()
41 {
42     WebInspector.Panel.call(this, "elements");
43     this.registerRequiredCSS("breadcrumbList.css");
44     this.registerRequiredCSS("elementsPanel.css");
45     this.registerRequiredCSS("textPrompt.css");
46     this.setHideOnDetach();
47
48     const initialSidebarWidth = 325;
49     const minimalContentWidthPercent = 34;
50     this.createSplitView(this.element, WebInspector.SplitView.SidebarPosition.Right, initialSidebarWidth);
51     this.splitView.minimalSidebarWidth = Preferences.minElementsSidebarWidth;
52     this.splitView.minimalMainWidthPercent = minimalContentWidthPercent;
53
54     this.contentElement = this.splitView.mainElement;
55     this.contentElement.id = "elements-content";
56     this.contentElement.addStyleClass("outline-disclosure");
57     this.contentElement.addStyleClass("source-code");
58     if (!WebInspector.settings.domWordWrap.get())
59         this.contentElement.classList.add("nowrap");
60     WebInspector.settings.domWordWrap.addChangeListener(this._domWordWrapSettingChanged.bind(this));
61
62     this.contentElement.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
63
64     this.treeOutline = new WebInspector.ElementsTreeOutline(true, true, false, this._populateContextMenu.bind(this), this._setPseudoClassForNodeId.bind(this));
65     this.treeOutline.wireToDomAgent();
66
67     this.treeOutline.addEventListener(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedNodeChanged, this);
68
69     this.crumbsElement = document.createElement("div");
70     this.crumbsElement.className = "crumbs";
71     this.crumbsElement.addEventListener("mousemove", this._mouseMovedInCrumbs.bind(this), false);
72     this.crumbsElement.addEventListener("mouseout", this._mouseMovedOutOfCrumbs.bind(this), false);
73
74     this.sidebarPanes = {};
75     this.sidebarPanes.computedStyle = new WebInspector.ComputedStyleSidebarPane();
76     this.sidebarPanes.styles = new WebInspector.StylesSidebarPane(this.sidebarPanes.computedStyle, this._setPseudoClassForNodeId.bind(this));
77     this.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
78     this.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
79     this.sidebarPanes.domBreakpoints = WebInspector.domBreakpointsSidebarPane;
80     this.sidebarPanes.eventListeners = new WebInspector.EventListenersSidebarPane();
81
82     this.sidebarPanes.styles.onexpand = this.updateStyles.bind(this);
83     this.sidebarPanes.metrics.onexpand = this.updateMetrics.bind(this);
84     this.sidebarPanes.properties.onexpand = this.updateProperties.bind(this);
85     this.sidebarPanes.eventListeners.onexpand = this.updateEventListeners.bind(this);
86
87     this.sidebarPanes.styles.expanded = true;
88
89     this.sidebarPanes.styles.addEventListener("style edited", this._stylesPaneEdited, this);
90     this.sidebarPanes.styles.addEventListener("style property toggled", this._stylesPaneEdited, this);
91     this.sidebarPanes.metrics.addEventListener("metrics edited", this._metricsPaneEdited, this);
92
93     for (var pane in this.sidebarPanes) {
94         this.sidebarElement.appendChild(this.sidebarPanes[pane].element);
95         if (this.sidebarPanes[pane].onattach)
96             this.sidebarPanes[pane].onattach();
97     }
98
99     this._registerShortcuts();
100
101     this._popoverHelper = new WebInspector.PopoverHelper(this.element, this._getPopoverAnchor.bind(this), this._showPopover.bind(this));
102     this._popoverHelper.setTimeout(0);
103
104     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeRemoved, this._nodeRemoved, this);
105     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.DocumentUpdated, this._documentUpdatedEvent, this);
106     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.InspectElementRequested, this._inspectElementRequested, this);
107
108     if (WebInspector.domAgent.existingDocument())
109         this._documentUpdated(WebInspector.domAgent.existingDocument());
110 }
111
112 WebInspector.ElementsPanel.prototype = {
113     get statusBarItems()
114     {
115         return [this.crumbsElement];
116     },
117
118     defaultFocusedElement: function()
119     {
120         return this.treeOutline.element;
121     },
122
123     statusBarResized: function()
124     {
125         this.updateBreadcrumbSizes();
126     },
127
128     wasShown: function()
129     {
130         // Attach heavy component lazily
131         if (this.treeOutline.element.parentElement !== this.contentElement)
132             this.contentElement.appendChild(this.treeOutline.element);
133
134         WebInspector.Panel.prototype.wasShown.call(this);
135
136         this.updateBreadcrumb();
137         this.treeOutline.updateSelection();
138         this.treeOutline.setVisible(true);
139
140         if (!this.treeOutline.rootDOMNode)
141             WebInspector.domAgent.requestDocument();
142
143         this.sidebarElement.insertBefore(this.sidebarPanes.domBreakpoints.element, this.sidebarPanes.eventListeners.element);
144     },
145
146     willHide: function()
147     {
148         WebInspector.domAgent.hideDOMNodeHighlight();
149         this.treeOutline.setVisible(false);
150         this._popoverHelper.hidePopover();
151
152         // Detach heavy component on hide
153         this.contentElement.removeChild(this.treeOutline.element);
154
155         for (var pane in this.sidebarPanes) {
156             if (this.sidebarPanes[pane].willHide)
157                 this.sidebarPanes[pane].willHide();
158         }
159
160         WebInspector.Panel.prototype.willHide.call(this);
161     },
162
163     onResize: function()
164     {
165         this.treeOutline.updateSelection();
166         this.updateBreadcrumbSizes();
167     },
168
169     /**
170      * @param {DOMAgent.NodeId} nodeId
171      * @param {string} pseudoClass
172      * @param {boolean} enable
173      */
174     _setPseudoClassForNodeId: function(nodeId, pseudoClass, enable)
175     {
176         var node = WebInspector.domAgent.nodeForId(nodeId);
177         if (!node)
178             return;
179
180         var pseudoClasses = node.getUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName);
181         if (enable) {
182             pseudoClasses = pseudoClasses || [];
183             if (pseudoClasses.indexOf(pseudoClass) >= 0)
184                 return;
185             pseudoClasses.push(pseudoClass);
186             node.setUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName, pseudoClasses);
187         } else {
188             if (!pseudoClasses || pseudoClasses.indexOf(pseudoClass) < 0)
189                 return;
190             pseudoClasses.remove(pseudoClass);
191             if (!pseudoClasses.length)
192                 node.removeUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName);
193         }
194
195         this.treeOutline.updateOpenCloseTags(node);
196         WebInspector.cssModel.forcePseudoState(node.id, node.getUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName));
197         this._metricsPaneEdited();
198         this._stylesPaneEdited();
199     },
200
201     _selectedNodeChanged: function()
202     {
203         var selectedNode = this.selectedDOMNode();
204         if (!selectedNode && this._lastValidSelectedNode)
205             this._selectedPathOnReset = this._lastValidSelectedNode.path();
206
207         this.updateBreadcrumb(false);
208
209         this._updateSidebars();
210
211         if (selectedNode) {
212             ConsoleAgent.addInspectedNode(selectedNode.id);
213             this._lastValidSelectedNode = selectedNode;
214         }
215         WebInspector.notifications.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged);
216     },
217
218     _updateSidebars: function()
219     {
220         for (var pane in this.sidebarPanes)
221            this.sidebarPanes[pane].needsUpdate = true;
222
223         this.updateStyles(true);
224         this.updateMetrics();
225         this.updateProperties();
226         this.updateEventListeners();
227     },
228
229     _reset: function()
230     {
231         delete this.currentQuery;
232     },
233
234     _documentUpdatedEvent: function(event)
235     {
236         this._documentUpdated(event.data);
237     },
238
239     _documentUpdated: function(inspectedRootDocument)
240     {
241         this._reset();
242         this.searchCanceled();
243
244         this.treeOutline.rootDOMNode = inspectedRootDocument;
245
246         if (!inspectedRootDocument) {
247             if (this.isShowing())
248                 WebInspector.domAgent.requestDocument();
249             return;
250         }
251
252         this.sidebarPanes.domBreakpoints.restoreBreakpoints();
253
254         /**
255          * @this {WebInspector.ElementsPanel}
256          * @param {WebInspector.DOMNode=} candidateFocusNode
257          */
258         function selectNode(candidateFocusNode)
259         {
260             if (!candidateFocusNode)
261                 candidateFocusNode = inspectedRootDocument.body || inspectedRootDocument.documentElement;
262
263             if (!candidateFocusNode)
264                 return;
265
266             this.selectDOMNode(candidateFocusNode);
267             if (this.treeOutline.selectedTreeElement)
268                 this.treeOutline.selectedTreeElement.expand();
269         }
270
271         function selectLastSelectedNode(nodeId)
272         {
273             if (this.selectedDOMNode()) {
274                 // Focused node has been explicitly set while reaching out for the last selected node.
275                 return;
276             }
277             var node = nodeId ? WebInspector.domAgent.nodeForId(nodeId) : null;
278             selectNode.call(this, node);
279         }
280
281         if (this._selectedPathOnReset)
282             WebInspector.domAgent.pushNodeByPathToFrontend(this._selectedPathOnReset, selectLastSelectedNode.bind(this));
283         else
284             selectNode.call(this);
285         delete this._selectedPathOnReset;
286     },
287
288     searchCanceled: function()
289     {
290         delete this._searchQuery;
291         this._hideSearchHighlights();
292
293         WebInspector.searchController.updateSearchMatchesCount(0, this);
294
295         delete this._currentSearchResultIndex;
296         delete this._searchResults;
297         WebInspector.domAgent.cancelSearch();
298     },
299
300     /**
301      * @param {string} query
302      */
303     performSearch: function(query)
304     {
305         // Call searchCanceled since it will reset everything we need before doing a new search.
306         this.searchCanceled();
307
308         const whitespaceTrimmedQuery = query.trim();
309         if (!whitespaceTrimmedQuery.length)
310             return;
311
312         this._searchQuery = query;
313
314         /**
315          * @param {number} resultCount
316          */
317         function resultCountCallback(resultCount)
318         {
319             WebInspector.searchController.updateSearchMatchesCount(resultCount, this);
320             if (!resultCount)
321                 return;
322
323             this._searchResults = new Array(resultCount);
324             this._currentSearchResultIndex = -1;
325             this.jumpToNextSearchResult();
326         }
327         WebInspector.domAgent.performSearch(whitespaceTrimmedQuery, resultCountCallback.bind(this));
328     },
329
330     _contextMenuEventFired: function(event)
331     {
332         function toggleWordWrap()
333         {
334             WebInspector.settings.domWordWrap.set(!WebInspector.settings.domWordWrap.get());
335         }
336
337         var contextMenu = new WebInspector.ContextMenu();
338         var populated = this.treeOutline.populateContextMenu(contextMenu, event);
339         if (populated)
340             contextMenu.appendSeparator();
341         contextMenu.appendCheckboxItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Word wrap" : "Word Wrap"), toggleWordWrap.bind(this), WebInspector.settings.domWordWrap.get());
342
343         contextMenu.show(event);
344     },
345
346     _domWordWrapSettingChanged: function(event)
347     {
348         if (event.data)
349             this.contentElement.removeStyleClass("nowrap");
350         else
351             this.contentElement.addStyleClass("nowrap");
352
353         var selectedNode = this.selectedDOMNode();
354         if (!selectedNode)
355             return;
356
357         var treeElement = this.treeOutline.findTreeElement(selectedNode);
358         if (treeElement)
359             treeElement.updateSelection(); // Recalculate selection highlight dimensions.
360     },
361
362     switchToAndFocus: function(node)
363     {
364         // Reset search restore.
365         WebInspector.searchController.cancelSearch();
366         WebInspector.inspectorView.setCurrentPanel(this);
367         this.selectDOMNode(node, true);
368     },
369
370     _populateContextMenu: function(contextMenu, node)
371     {
372         // Add debbuging-related actions
373         contextMenu.appendSeparator();
374         var pane = this.sidebarPanes.domBreakpoints;
375         pane.populateNodeContextMenu(node, contextMenu);
376     },
377
378     _getPopoverAnchor: function(element)
379     {
380         var anchor = element.enclosingNodeOrSelfWithClass("webkit-html-resource-link");
381         if (anchor) {
382             if (!anchor.href)
383                 return null;
384
385             var resource = WebInspector.resourceTreeModel.resourceForURL(anchor.href);
386             if (!resource || resource.type !== WebInspector.resourceTypes.Image)
387                 return null;
388
389             anchor.removeAttribute("title");
390         }
391         return anchor;
392     },
393     
394     _loadDimensionsForNode: function(treeElement, callback)
395     {
396         // We get here for CSS properties, too, so bail out early for non-DOM treeElements.
397         if (treeElement.treeOutline !== this.treeOutline) {
398             callback();
399             return;
400         }
401         
402         var node = /** @type {WebInspector.DOMNode} */ treeElement.representedObject;
403
404         if (!node.nodeName() || node.nodeName().toLowerCase() !== "img") {
405             callback();
406             return;
407         }
408
409         WebInspector.RemoteObject.resolveNode(node, "", resolvedNode);
410
411         function resolvedNode(object)
412         {
413             if (!object) {
414                 callback();
415                 return;
416             }
417
418             object.callFunctionJSON(dimensions, undefined, callback);
419             object.release();
420
421             function dimensions()
422             {
423                 return { offsetWidth: this.offsetWidth, offsetHeight: this.offsetHeight, naturalWidth: this.naturalWidth, naturalHeight: this.naturalHeight };
424             }
425         }
426     },
427
428     /**
429      * @param {Element} anchor
430      * @param {WebInspector.Popover} popover
431      */
432     _showPopover: function(anchor, popover)
433     {
434         var listItem = anchor.enclosingNodeOrSelfWithNodeName("li");
435         if (listItem && listItem.treeElement)
436             this._loadDimensionsForNode(listItem.treeElement, WebInspector.buildImagePreviewContents.bind(WebInspector, anchor.href, true, showPopover));
437         else
438             WebInspector.buildImagePreviewContents(anchor.href, true, showPopover);
439
440         /**
441          * @param {Element=} contents
442          */
443         function showPopover(contents)
444         {
445             if (!contents)
446                 return;
447             popover.setCanShrink(false);
448             popover.show(contents, anchor);
449         }
450     },
451
452     jumpToNextSearchResult: function()
453     {
454         if (!this._searchResults)
455             return;
456
457         this._hideSearchHighlights();
458         if (++this._currentSearchResultIndex >= this._searchResults.length)
459             this._currentSearchResultIndex = 0;
460
461         this._highlightCurrentSearchResult();
462     },
463
464     jumpToPreviousSearchResult: function()
465     {
466         if (!this._searchResults)
467             return;
468
469         this._hideSearchHighlights();
470         if (--this._currentSearchResultIndex < 0)
471             this._currentSearchResultIndex = (this._searchResults.length - 1);
472
473         this._highlightCurrentSearchResult();
474         return true;
475     },
476
477     _highlightCurrentSearchResult: function()
478     {
479         var index = this._currentSearchResultIndex;
480         var searchResults = this._searchResults;
481         var searchResult = searchResults[index];
482
483         if (searchResult === null) {
484             WebInspector.searchController.updateCurrentMatchIndex(index, this);
485             return;
486         }
487
488         if (typeof searchResult === "undefined") {
489             // No data for slot, request it.
490             function callback(node)
491             {
492                 searchResults[index] = node || null;
493                 this._highlightCurrentSearchResult();
494             }
495             WebInspector.domAgent.searchResult(index, callback.bind(this));
496             return;
497         }
498
499         WebInspector.searchController.updateCurrentMatchIndex(index, this);
500
501         var treeElement = this.treeOutline.findTreeElement(searchResult);
502         if (treeElement) {
503             treeElement.highlightSearchResults(this._searchQuery);
504             treeElement.reveal();
505         }
506     },
507
508     _hideSearchHighlights: function()
509     {
510         if (!this._searchResults)
511             return;
512         var searchResult = this._searchResults[this._currentSearchResultIndex];
513         if (!searchResult)
514             return;
515         var treeElement = this.treeOutline.findTreeElement(searchResult);
516         if (treeElement)
517             treeElement.hideSearchHighlights();
518     },
519
520     selectedDOMNode: function()
521     {
522         return this.treeOutline.selectedDOMNode();
523     },
524
525     /**
526      * @param {boolean=} focus
527      */
528     selectDOMNode: function(node, focus)
529     {
530         this.treeOutline.selectDOMNode(node, focus);
531     },
532
533     _nodeRemoved: function(event)
534     {
535         if (!this.isShowing())
536             return;
537
538         var crumbs = this.crumbsElement;
539         for (var crumb = crumbs.firstChild; crumb; crumb = crumb.nextSibling) {
540             if (crumb.representedObject === event.data.node) {
541                 this.updateBreadcrumb(true);
542                 return;
543             }
544         }
545     },
546
547     _stylesPaneEdited: function()
548     {
549         // Once styles are edited, the Metrics pane should be updated.
550         this.sidebarPanes.metrics.needsUpdate = true;
551         this.updateMetrics();
552     },
553
554     _metricsPaneEdited: function()
555     {
556         // Once metrics are edited, the Styles pane should be updated.
557         this.sidebarPanes.styles.needsUpdate = true;
558         this.updateStyles(true);
559     },
560
561     _mouseMovedInCrumbs: function(event)
562     {
563         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
564         var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb");
565
566         WebInspector.domAgent.highlightDOMNode(crumbElement ? crumbElement.representedObject.id : 0);
567
568         if ("_mouseOutOfCrumbsTimeout" in this) {
569             clearTimeout(this._mouseOutOfCrumbsTimeout);
570             delete this._mouseOutOfCrumbsTimeout;
571         }
572     },
573
574     _mouseMovedOutOfCrumbs: function(event)
575     {
576         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
577         if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.crumbsElement))
578             return;
579
580         WebInspector.domAgent.hideDOMNodeHighlight();
581
582         this._mouseOutOfCrumbsTimeout = setTimeout(this.updateBreadcrumbSizes.bind(this), 1000);
583     },
584
585     /**
586      * @param {boolean=} forceUpdate
587      */
588     updateBreadcrumb: function(forceUpdate)
589     {
590         if (!this.isShowing())
591             return;
592
593         var crumbs = this.crumbsElement;
594
595         var handled = false;
596         var foundRoot = false;
597         var crumb = crumbs.firstChild;
598         while (crumb) {
599             if (crumb.representedObject === this.treeOutline.rootDOMNode)
600                 foundRoot = true;
601
602             if (foundRoot)
603                 crumb.addStyleClass("dimmed");
604             else
605                 crumb.removeStyleClass("dimmed");
606
607             if (crumb.representedObject === this.selectedDOMNode()) {
608                 crumb.addStyleClass("selected");
609                 handled = true;
610             } else {
611                 crumb.removeStyleClass("selected");
612             }
613
614             crumb = crumb.nextSibling;
615         }
616
617         if (handled && !forceUpdate) {
618             // We don't need to rebuild the crumbs, but we need to adjust sizes
619             // to reflect the new focused or root node.
620             this.updateBreadcrumbSizes();
621             return;
622         }
623
624         crumbs.removeChildren();
625
626         var panel = this;
627
628         function selectCrumbFunction(event)
629         {
630             var crumb = event.currentTarget;
631             if (crumb.hasStyleClass("collapsed")) {
632                 // Clicking a collapsed crumb will expose the hidden crumbs.
633                 if (crumb === panel.crumbsElement.firstChild) {
634                     // If the focused crumb is the first child, pick the farthest crumb
635                     // that is still hidden. This allows the user to expose every crumb.
636                     var currentCrumb = crumb;
637                     while (currentCrumb) {
638                         var hidden = currentCrumb.hasStyleClass("hidden");
639                         var collapsed = currentCrumb.hasStyleClass("collapsed");
640                         if (!hidden && !collapsed)
641                             break;
642                         crumb = currentCrumb;
643                         currentCrumb = currentCrumb.nextSibling;
644                     }
645                 }
646
647                 panel.updateBreadcrumbSizes(crumb);
648             } else
649                 panel.selectDOMNode(crumb.representedObject, true);
650
651             event.preventDefault();
652         }
653
654         foundRoot = false;
655         for (var current = this.selectedDOMNode(); current; current = current.parentNode) {
656             if (current.nodeType() === Node.DOCUMENT_NODE)
657                 continue;
658
659             if (current === this.treeOutline.rootDOMNode)
660                 foundRoot = true;
661
662             crumb = document.createElement("span");
663             crumb.className = "crumb";
664             crumb.representedObject = current;
665             crumb.addEventListener("mousedown", selectCrumbFunction, false);
666
667             var crumbTitle;
668             switch (current.nodeType()) {
669                 case Node.ELEMENT_NODE:
670                     WebInspector.DOMPresentationUtils.decorateNodeLabel(current, crumb);
671                     break;
672
673                 case Node.TEXT_NODE:
674                     crumbTitle = WebInspector.UIString("(text)");
675                     break
676
677                 case Node.COMMENT_NODE:
678                     crumbTitle = "<!-->";
679                     break;
680
681                 case Node.DOCUMENT_TYPE_NODE:
682                     crumbTitle = "<!DOCTYPE>";
683                     break;
684
685                 default:
686                     crumbTitle = current.nodeNameInCorrectCase();
687             }
688
689             if (!crumb.childNodes.length) {
690                 var nameElement = document.createElement("span");
691                 nameElement.textContent = crumbTitle;
692                 crumb.appendChild(nameElement);
693                 crumb.title = crumbTitle;
694             }
695
696             if (foundRoot)
697                 crumb.addStyleClass("dimmed");
698             if (current === this.selectedDOMNode())
699                 crumb.addStyleClass("selected");
700             if (!crumbs.childNodes.length)
701                 crumb.addStyleClass("end");
702
703             crumbs.appendChild(crumb);
704         }
705
706         if (crumbs.hasChildNodes())
707             crumbs.lastChild.addStyleClass("start");
708
709         this.updateBreadcrumbSizes();
710     },
711
712     /**
713      * @param {Element=} focusedCrumb
714      */
715     updateBreadcrumbSizes: function(focusedCrumb)
716     {
717         if (!this.isShowing())
718             return;
719
720         if (document.body.offsetWidth <= 0) {
721             // The stylesheet hasn't loaded yet or the window is closed,
722             // so we can't calculate what is need. Return early.
723             return;
724         }
725
726         var crumbs = this.crumbsElement;
727         if (!crumbs.childNodes.length || crumbs.offsetWidth <= 0)
728             return; // No crumbs, do nothing.
729
730         // A Zero index is the right most child crumb in the breadcrumb.
731         var selectedIndex = 0;
732         var focusedIndex = 0;
733         var selectedCrumb;
734
735         var i = 0;
736         var crumb = crumbs.firstChild;
737         while (crumb) {
738             // Find the selected crumb and index.
739             if (!selectedCrumb && crumb.hasStyleClass("selected")) {
740                 selectedCrumb = crumb;
741                 selectedIndex = i;
742             }
743
744             // Find the focused crumb index.
745             if (crumb === focusedCrumb)
746                 focusedIndex = i;
747
748             // Remove any styles that affect size before
749             // deciding to shorten any crumbs.
750             if (crumb !== crumbs.lastChild)
751                 crumb.removeStyleClass("start");
752             if (crumb !== crumbs.firstChild)
753                 crumb.removeStyleClass("end");
754
755             crumb.removeStyleClass("compact");
756             crumb.removeStyleClass("collapsed");
757             crumb.removeStyleClass("hidden");
758
759             crumb = crumb.nextSibling;
760             ++i;
761         }
762
763         // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs().
764         // The order of the crumbs in the document is opposite of the visual order.
765         crumbs.firstChild.addStyleClass("end");
766         crumbs.lastChild.addStyleClass("start");
767
768         function crumbsAreSmallerThanContainer()
769         {
770             var rightPadding = 20;
771             var errorWarningElement = document.getElementById("error-warning-count");
772             if (!WebInspector.drawer.visible && errorWarningElement)
773                 rightPadding += errorWarningElement.offsetWidth;
774             return ((crumbs.totalOffsetLeft() + crumbs.offsetWidth + rightPadding) < window.innerWidth);
775         }
776
777         if (crumbsAreSmallerThanContainer())
778             return; // No need to compact the crumbs, they all fit at full size.
779
780         var BothSides = 0;
781         var AncestorSide = -1;
782         var ChildSide = 1;
783
784         /**
785          * @param {boolean=} significantCrumb
786          */
787         function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb)
788         {
789             if (!significantCrumb)
790                 significantCrumb = (focusedCrumb || selectedCrumb);
791
792             if (significantCrumb === selectedCrumb)
793                 var significantIndex = selectedIndex;
794             else if (significantCrumb === focusedCrumb)
795                 var significantIndex = focusedIndex;
796             else {
797                 var significantIndex = 0;
798                 for (var i = 0; i < crumbs.childNodes.length; ++i) {
799                     if (crumbs.childNodes[i] === significantCrumb) {
800                         significantIndex = i;
801                         break;
802                     }
803                 }
804             }
805
806             function shrinkCrumbAtIndex(index)
807             {
808                 var shrinkCrumb = crumbs.childNodes[index];
809                 if (shrinkCrumb && shrinkCrumb !== significantCrumb)
810                     shrinkingFunction(shrinkCrumb);
811                 if (crumbsAreSmallerThanContainer())
812                     return true; // No need to compact the crumbs more.
813                 return false;
814             }
815
816             // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
817             // fit in the container or we run out of crumbs to shrink.
818             if (direction) {
819                 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
820                 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
821                 while (index !== significantIndex) {
822                     if (shrinkCrumbAtIndex(index))
823                         return true;
824                     index += (direction > 0 ? 1 : -1);
825                 }
826             } else {
827                 // Crumbs are shrunk in order of descending distance from the signifcant crumb,
828                 // with a tie going to child crumbs.
829                 var startIndex = 0;
830                 var endIndex = crumbs.childNodes.length - 1;
831                 while (startIndex != significantIndex || endIndex != significantIndex) {
832                     var startDistance = significantIndex - startIndex;
833                     var endDistance = endIndex - significantIndex;
834                     if (startDistance >= endDistance)
835                         var index = startIndex++;
836                     else
837                         var index = endIndex--;
838                     if (shrinkCrumbAtIndex(index))
839                         return true;
840                 }
841             }
842
843             // We are not small enough yet, return false so the caller knows.
844             return false;
845         }
846
847         function coalesceCollapsedCrumbs()
848         {
849             var crumb = crumbs.firstChild;
850             var collapsedRun = false;
851             var newStartNeeded = false;
852             var newEndNeeded = false;
853             while (crumb) {
854                 var hidden = crumb.hasStyleClass("hidden");
855                 if (!hidden) {
856                     var collapsed = crumb.hasStyleClass("collapsed");
857                     if (collapsedRun && collapsed) {
858                         crumb.addStyleClass("hidden");
859                         crumb.removeStyleClass("compact");
860                         crumb.removeStyleClass("collapsed");
861
862                         if (crumb.hasStyleClass("start")) {
863                             crumb.removeStyleClass("start");
864                             newStartNeeded = true;
865                         }
866
867                         if (crumb.hasStyleClass("end")) {
868                             crumb.removeStyleClass("end");
869                             newEndNeeded = true;
870                         }
871
872                         continue;
873                     }
874
875                     collapsedRun = collapsed;
876
877                     if (newEndNeeded) {
878                         newEndNeeded = false;
879                         crumb.addStyleClass("end");
880                     }
881                 } else
882                     collapsedRun = true;
883                 crumb = crumb.nextSibling;
884             }
885
886             if (newStartNeeded) {
887                 crumb = crumbs.lastChild;
888                 while (crumb) {
889                     if (!crumb.hasStyleClass("hidden")) {
890                         crumb.addStyleClass("start");
891                         break;
892                     }
893                     crumb = crumb.previousSibling;
894                 }
895             }
896         }
897
898         function compact(crumb)
899         {
900             if (crumb.hasStyleClass("hidden"))
901                 return;
902             crumb.addStyleClass("compact");
903         }
904
905         function collapse(crumb, dontCoalesce)
906         {
907             if (crumb.hasStyleClass("hidden"))
908                 return;
909             crumb.addStyleClass("collapsed");
910             crumb.removeStyleClass("compact");
911             if (!dontCoalesce)
912                 coalesceCollapsedCrumbs();
913         }
914
915         function compactDimmed(crumb)
916         {
917             if (crumb.hasStyleClass("dimmed"))
918                 compact(crumb);
919         }
920
921         function collapseDimmed(crumb)
922         {
923             if (crumb.hasStyleClass("dimmed"))
924                 collapse(crumb, false);
925         }
926
927         if (!focusedCrumb) {
928             // When not focused on a crumb we can be biased and collapse less important
929             // crumbs that the user might not care much about.
930
931             // Compact child crumbs.
932             if (makeCrumbsSmaller(compact, ChildSide))
933                 return;
934
935             // Collapse child crumbs.
936             if (makeCrumbsSmaller(collapse, ChildSide))
937                 return;
938
939             // Compact dimmed ancestor crumbs.
940             if (makeCrumbsSmaller(compactDimmed, AncestorSide))
941                 return;
942
943             // Collapse dimmed ancestor crumbs.
944             if (makeCrumbsSmaller(collapseDimmed, AncestorSide))
945                 return;
946         }
947
948         // Compact ancestor crumbs, or from both sides if focused.
949         if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide)))
950             return;
951
952         // Collapse ancestor crumbs, or from both sides if focused.
953         if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide)))
954             return;
955
956         if (!selectedCrumb)
957             return;
958
959         // Compact the selected crumb.
960         compact(selectedCrumb);
961         if (crumbsAreSmallerThanContainer())
962             return;
963
964         // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
965         collapse(selectedCrumb, true);
966     },
967
968     updateStyles: function(forceUpdate)
969     {
970         var stylesSidebarPane = this.sidebarPanes.styles;
971         var computedStylePane = this.sidebarPanes.computedStyle;
972         if ((!stylesSidebarPane.expanded && !computedStylePane.expanded) || !stylesSidebarPane.needsUpdate)
973             return;
974
975         stylesSidebarPane.update(this.selectedDOMNode(), forceUpdate);
976         stylesSidebarPane.needsUpdate = false;
977     },
978
979     updateMetrics: function()
980     {
981         var metricsSidebarPane = this.sidebarPanes.metrics;
982         if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate)
983             return;
984
985         metricsSidebarPane.update(this.selectedDOMNode());
986         metricsSidebarPane.needsUpdate = false;
987     },
988
989     updateProperties: function()
990     {
991         var propertiesSidebarPane = this.sidebarPanes.properties;
992         if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate)
993             return;
994
995         propertiesSidebarPane.update(this.selectedDOMNode());
996         propertiesSidebarPane.needsUpdate = false;
997     },
998
999     updateEventListeners: function()
1000     {
1001         var eventListenersSidebarPane = this.sidebarPanes.eventListeners;
1002         if (!eventListenersSidebarPane.expanded || !eventListenersSidebarPane.needsUpdate)
1003             return;
1004
1005         eventListenersSidebarPane.update(this.selectedDOMNode());
1006         eventListenersSidebarPane.needsUpdate = false;
1007     },
1008
1009     _registerShortcuts: function()
1010     {
1011         var shortcut = WebInspector.KeyboardShortcut;
1012         var section = WebInspector.shortcutsScreen.section(WebInspector.UIString("Elements Panel"));
1013         var keys = [
1014             shortcut.shortcutToString(shortcut.Keys.Up),
1015             shortcut.shortcutToString(shortcut.Keys.Down)
1016         ];
1017         section.addRelatedKeys(keys, WebInspector.UIString("Navigate elements"));
1018
1019         keys = [
1020             shortcut.shortcutToString(shortcut.Keys.Right),
1021             shortcut.shortcutToString(shortcut.Keys.Left)
1022         ];
1023         section.addRelatedKeys(keys, WebInspector.UIString("Expand/collapse"));
1024         section.addKey(shortcut.shortcutToString(shortcut.Keys.Enter), WebInspector.UIString("Edit attribute"));
1025         section.addKey(shortcut.shortcutToString(shortcut.Keys.F2), WebInspector.UIString("Toggle edit as HTML"));
1026
1027         this.sidebarPanes.styles.registerShortcuts();
1028     },
1029
1030     handleShortcut: function(event)
1031     {
1032         if (WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) && !event.shiftKey && event.keyIdentifier === "U+005A") { // Z key
1033             WebInspector.domAgent.undo(this._updateSidebars.bind(this));
1034             event.handled = true;
1035             return;
1036         }
1037
1038         var isRedoKey = WebInspector.isMac() ? event.metaKey && event.shiftKey && event.keyIdentifier === "U+005A" : // Z key
1039                                                event.ctrlKey && event.keyIdentifier === "U+0059"; // Y key
1040         if (isRedoKey) {
1041             DOMAgent.redo(this._updateSidebars.bind(this));
1042             event.handled = true;
1043             return;
1044         }
1045
1046         this.treeOutline.handleShortcut(event);
1047     },
1048
1049     handleCopyEvent: function(event)
1050     {
1051         // Don't prevent the normal copy if the user has a selection.
1052         if (!window.getSelection().isCollapsed)
1053             return;
1054         event.clipboardData.clearData();
1055         event.preventDefault();
1056         this.selectedDOMNode().copyNode();
1057     },
1058
1059     sidebarResized: function(event)
1060     {
1061         this.treeOutline.updateSelection();
1062     },
1063
1064     _inspectElementRequested: function(event)
1065     {
1066         var node = event.data;
1067         this.revealAndSelectNode(node.id);
1068     },
1069
1070     revealAndSelectNode: function(nodeId)
1071     {
1072         WebInspector.inspectorView.setCurrentPanel(this);
1073
1074         var node = WebInspector.domAgent.nodeForId(nodeId);
1075         if (!node)
1076             return;
1077
1078         WebInspector.domAgent.highlightDOMNodeForTwoSeconds(nodeId);
1079         this.selectDOMNode(node, true);
1080     }
1081 }
1082
1083 WebInspector.ElementsPanel.prototype.__proto__ = WebInspector.Panel.prototype;