Home | History | Annotate | Download | only in front-end
      1 /*
      2  * Copyright (C) 2007, 2008 Apple Inc.  All rights reserved.
      3  * Copyright (C) 2008 Matt Lilek <webkit (at) 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 WebInspector.ElementsPanel = function()
     32 {
     33     WebInspector.Panel.call(this, "elements");
     34 
     35     this.contentElement = document.createElement("div");
     36     this.contentElement.id = "elements-content";
     37     this.contentElement.className = "outline-disclosure source-code";
     38     if (!WebInspector.settings.domWordWrap)
     39         this.contentElement.classList.add("nowrap");
     40 
     41     this.contentElement.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
     42 
     43     this.treeOutline = new WebInspector.ElementsTreeOutline();
     44     this.treeOutline.panel = this;
     45     this.treeOutline.includeRootDOMNode = false;
     46     this.treeOutline.selectEnabled = true;
     47 
     48     this.treeOutline.focusedNodeChanged = function(forceUpdate)
     49     {
     50         if (this.panel.visible && WebInspector.currentFocusElement !== document.getElementById("search"))
     51             WebInspector.currentFocusElement = this.element;
     52 
     53         this.panel.updateBreadcrumb(forceUpdate);
     54 
     55         for (var pane in this.panel.sidebarPanes)
     56            this.panel.sidebarPanes[pane].needsUpdate = true;
     57 
     58         this.panel.updateStyles(true);
     59         this.panel.updateMetrics();
     60         this.panel.updateProperties();
     61         this.panel.updateEventListeners();
     62 
     63         if (this._focusedDOMNode) {
     64             ConsoleAgent.addInspectedNode(this._focusedDOMNode.id);
     65             WebInspector.extensionServer.notifyObjectSelected(this.panel.name);
     66         }
     67     };
     68 
     69     this.contentElement.appendChild(this.treeOutline.element);
     70 
     71     this.crumbsElement = document.createElement("div");
     72     this.crumbsElement.className = "crumbs";
     73     this.crumbsElement.addEventListener("mousemove", this._mouseMovedInCrumbs.bind(this), false);
     74     this.crumbsElement.addEventListener("mouseout", this._mouseMovedOutOfCrumbs.bind(this), false);
     75 
     76     this.sidebarPanes = {};
     77     this.sidebarPanes.computedStyle = new WebInspector.ComputedStyleSidebarPane();
     78     this.sidebarPanes.styles = new WebInspector.StylesSidebarPane(this.sidebarPanes.computedStyle);
     79     this.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
     80     this.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
     81     if (Preferences.nativeInstrumentationEnabled)
     82         this.sidebarPanes.domBreakpoints = WebInspector.domBreakpointsSidebarPane;
     83     this.sidebarPanes.eventListeners = new WebInspector.EventListenersSidebarPane();
     84 
     85     this.sidebarPanes.styles.onexpand = this.updateStyles.bind(this);
     86     this.sidebarPanes.metrics.onexpand = this.updateMetrics.bind(this);
     87     this.sidebarPanes.properties.onexpand = this.updateProperties.bind(this);
     88     this.sidebarPanes.eventListeners.onexpand = this.updateEventListeners.bind(this);
     89 
     90     this.sidebarPanes.styles.expanded = true;
     91 
     92     this.sidebarPanes.styles.addEventListener("style edited", this._stylesPaneEdited, this);
     93     this.sidebarPanes.styles.addEventListener("style property toggled", this._stylesPaneEdited, this);
     94     this.sidebarPanes.metrics.addEventListener("metrics edited", this._metricsPaneEdited, this);
     95     WebInspector.cssModel.addEventListener("stylesheet changed", this._styleSheetChanged, this);
     96 
     97     this.sidebarElement = document.createElement("div");
     98     this.sidebarElement.id = "elements-sidebar";
     99 
    100     for (var pane in this.sidebarPanes)
    101         this.sidebarElement.appendChild(this.sidebarPanes[pane].element);
    102 
    103     this.sidebarResizeElement = document.createElement("div");
    104     this.sidebarResizeElement.className = "sidebar-resizer-vertical";
    105     this.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false);
    106 
    107     this._nodeSearchButton = new WebInspector.StatusBarButton(WebInspector.UIString("Select an element in the page to inspect it."), "node-search-status-bar-item");
    108     this._nodeSearchButton.addEventListener("click", this.toggleSearchingForNode.bind(this), false);
    109 
    110     this.element.appendChild(this.contentElement);
    111     this.element.appendChild(this.sidebarElement);
    112     this.element.appendChild(this.sidebarResizeElement);
    113 
    114     this._registerShortcuts();
    115 
    116     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeInserted, this._nodeInserted, this);
    117     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeRemoved, this._nodeRemoved, this);
    118     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrModified, this._attributesUpdated, this);
    119     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.CharacterDataModified, this._characterDataModified, this);
    120     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.DocumentUpdated, this._documentUpdated, this);
    121     WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.ChildNodeCountUpdated, this._childNodeCountUpdated, this);
    122 
    123     this.recentlyModifiedNodes = [];
    124 }
    125 
    126 WebInspector.ElementsPanel.prototype = {
    127     get toolbarItemLabel()
    128     {
    129         return WebInspector.UIString("Elements");
    130     },
    131 
    132     get statusBarItems()
    133     {
    134         return [this._nodeSearchButton.element, this.crumbsElement];
    135     },
    136 
    137     get defaultFocusedElement()
    138     {
    139         return this.treeOutline.element;
    140     },
    141 
    142     updateStatusBarItems: function()
    143     {
    144         this.updateBreadcrumbSizes();
    145     },
    146 
    147     show: function()
    148     {
    149         WebInspector.Panel.prototype.show.call(this);
    150         this.sidebarResizeElement.style.right = (this.sidebarElement.offsetWidth - 3) + "px";
    151         this.updateBreadcrumb();
    152         this.treeOutline.updateSelection();
    153         if (this.recentlyModifiedNodes.length)
    154             this.updateModifiedNodes();
    155 
    156         if (Preferences.nativeInstrumentationEnabled)
    157             this.sidebarElement.insertBefore(this.sidebarPanes.domBreakpoints.element, this.sidebarPanes.eventListeners.element);
    158 
    159         if (!this.rootDOMNode)
    160             WebInspector.domAgent.requestDocument();
    161     },
    162 
    163     hide: function()
    164     {
    165         WebInspector.Panel.prototype.hide.call(this);
    166 
    167         WebInspector.highlightDOMNode(0);
    168         this.setSearchingForNode(false);
    169     },
    170 
    171     resize: function()
    172     {
    173         this.treeOutline.updateSelection();
    174         this.updateBreadcrumbSizes();
    175     },
    176 
    177     _reset: function()
    178     {
    179         if (this.focusedDOMNode)
    180             this._selectedPathOnReset = this.focusedDOMNode.path();
    181 
    182         this.rootDOMNode = null;
    183         this.focusedDOMNode = null;
    184 
    185         WebInspector.highlightDOMNode(0);
    186 
    187         this.recentlyModifiedNodes = [];
    188 
    189         delete this.currentQuery;
    190     },
    191 
    192     _documentUpdated: function(event)
    193     {
    194         this._setDocument(event.data);
    195     },
    196 
    197     _setDocument: function(inspectedRootDocument)
    198     {
    199         this._reset();
    200         this.searchCanceled();
    201 
    202         if (!inspectedRootDocument)
    203             return;
    204 
    205         if (Preferences.nativeInstrumentationEnabled)
    206             this.sidebarPanes.domBreakpoints.restoreBreakpoints();
    207 
    208         this.rootDOMNode = inspectedRootDocument;
    209 
    210         function selectNode(candidateFocusNode)
    211         {
    212             if (!candidateFocusNode)
    213                 candidateFocusNode = inspectedRootDocument.body || inspectedRootDocument.documentElement;
    214 
    215             if (!candidateFocusNode)
    216                 return;
    217 
    218             this.focusedDOMNode = candidateFocusNode;
    219             if (this.treeOutline.selectedTreeElement)
    220                 this.treeOutline.selectedTreeElement.expand();
    221         }
    222 
    223         function selectLastSelectedNode(nodeId)
    224         {
    225             if (this.focusedDOMNode) {
    226                 // Focused node has been explicitly set while reaching out for the last selected node.
    227                 return;
    228             }
    229             var node = nodeId ? WebInspector.domAgent.nodeForId(nodeId) : 0;
    230             selectNode.call(this, node);
    231         }
    232 
    233         if (this._selectedPathOnReset)
    234             WebInspector.domAgent.pushNodeByPathToFrontend(this._selectedPathOnReset, selectLastSelectedNode.bind(this));
    235         else
    236             selectNode.call(this);
    237         delete this._selectedPathOnReset;
    238     },
    239 
    240     searchCanceled: function()
    241     {
    242         delete this._searchQuery;
    243         this._hideSearchHighlights();
    244 
    245         WebInspector.searchController.updateSearchMatchesCount(0, this);
    246 
    247         delete this._currentSearchResultIndex;
    248         this._searchResults = [];
    249         WebInspector.domAgent.cancelSearch();
    250     },
    251 
    252     performSearch: function(query)
    253     {
    254         // Call searchCanceled since it will reset everything we need before doing a new search.
    255         this.searchCanceled();
    256 
    257         const whitespaceTrimmedQuery = query.trim();
    258         if (!whitespaceTrimmedQuery.length)
    259             return;
    260 
    261         this._updatedMatchCountOnce = false;
    262         this._matchesCountUpdateTimeout = null;
    263         this._searchQuery = query;
    264 
    265         WebInspector.domAgent.performSearch(whitespaceTrimmedQuery, this._addNodesToSearchResult.bind(this));
    266     },
    267 
    268     _contextMenuEventFired: function(event)
    269     {
    270         function isTextWrapped()
    271         {
    272             return !this.contentElement.hasStyleClass("nowrap");
    273         }
    274 
    275         function toggleWordWrap()
    276         {
    277             this.contentElement.classList.toggle("nowrap");
    278             WebInspector.settings.domWordWrap = !this.contentElement.classList.contains("nowrap");
    279 
    280             var treeElement = this.treeOutline.findTreeElement(this.focusedDOMNode);
    281             if (treeElement)
    282                 treeElement.updateSelection(); // Recalculate selection highlight dimensions.
    283         }
    284 
    285         var contextMenu = new WebInspector.ContextMenu();
    286 
    287         var populated = this.treeOutline.populateContextMenu(contextMenu, event);
    288         if (populated)
    289             contextMenu.appendSeparator();
    290         contextMenu.appendCheckboxItem(WebInspector.UIString("Word Wrap"), toggleWordWrap.bind(this), isTextWrapped.call(this));
    291 
    292         contextMenu.show(event);
    293     },
    294 
    295     populateHrefContextMenu: function(contextMenu, event, anchorElement)
    296     {
    297         if (!anchorElement.href)
    298             return false;
    299 
    300         var resourceURL = WebInspector.resourceURLForRelatedNode(this.focusedDOMNode, anchorElement.href);
    301         if (!resourceURL)
    302             return false;
    303 
    304         // Add resource-related actions.
    305         contextMenu.appendItem(WebInspector.openLinkExternallyLabel(), WebInspector.openResource.bind(null, resourceURL, false));
    306         if (WebInspector.resourceForURL(resourceURL))
    307             contextMenu.appendItem(WebInspector.UIString("Open Link in Resources Panel"), WebInspector.openResource.bind(null, resourceURL, true));
    308         return true;
    309     },
    310 
    311     switchToAndFocus: function(node)
    312     {
    313         // Reset search restore.
    314         WebInspector.searchController.cancelSearch();
    315         WebInspector.currentPanel = this;
    316         this.focusedDOMNode = node;
    317     },
    318 
    319     _updateMatchesCount: function()
    320     {
    321         WebInspector.searchController.updateSearchMatchesCount(this._searchResults.length, this);
    322         this._matchesCountUpdateTimeout = null;
    323         this._updatedMatchCountOnce = true;
    324     },
    325 
    326     _updateMatchesCountSoon: function()
    327     {
    328         if (!this._updatedMatchCountOnce)
    329             return this._updateMatchesCount();
    330         if (this._matchesCountUpdateTimeout)
    331             return;
    332         // Update the matches count every half-second so it doesn't feel twitchy.
    333         this._matchesCountUpdateTimeout = setTimeout(this._updateMatchesCount.bind(this), 500);
    334     },
    335 
    336     _addNodesToSearchResult: function(nodeIds)
    337     {
    338         if (!nodeIds.length)
    339             return;
    340 
    341         var oldSearchResultIndex = this._currentSearchResultIndex;
    342         for (var i = 0; i < nodeIds.length; ++i) {
    343             var nodeId = nodeIds[i];
    344             var node = WebInspector.domAgent.nodeForId(nodeId);
    345             if (!node)
    346                 continue;
    347 
    348             this._currentSearchResultIndex = 0;
    349             this._searchResults.push(node);
    350         }
    351 
    352         // Avoid invocations of highlighting for every chunk of nodeIds.
    353         if (oldSearchResultIndex !== this._currentSearchResultIndex)
    354             this._highlightCurrentSearchResult();
    355         this._updateMatchesCountSoon();
    356     },
    357 
    358     jumpToNextSearchResult: function()
    359     {
    360         if (!this._searchResults || !this._searchResults.length)
    361             return;
    362 
    363         if (++this._currentSearchResultIndex >= this._searchResults.length)
    364             this._currentSearchResultIndex = 0;
    365         this._highlightCurrentSearchResult();
    366     },
    367 
    368     jumpToPreviousSearchResult: function()
    369     {
    370         if (!this._searchResults || !this._searchResults.length)
    371             return;
    372 
    373         if (--this._currentSearchResultIndex < 0)
    374             this._currentSearchResultIndex = (this._searchResults.length - 1);
    375         this._highlightCurrentSearchResult();
    376     },
    377 
    378     _highlightCurrentSearchResult: function()
    379     {
    380         this._hideSearchHighlights();
    381         var node = this._searchResults[this._currentSearchResultIndex];
    382         var treeElement = this.treeOutline.findTreeElement(node);
    383         if (treeElement) {
    384             treeElement.highlightSearchResults(this._searchQuery);
    385             treeElement.reveal();
    386         }
    387     },
    388 
    389     _hideSearchHighlights: function(node)
    390     {
    391         for (var i = 0; this._searchResults && i < this._searchResults.length; ++i) {
    392             var node = this._searchResults[i];
    393             var treeElement = this.treeOutline.findTreeElement(node);
    394             if (treeElement)
    395                 treeElement.highlightSearchResults(null);
    396         }
    397     },
    398 
    399     renameSelector: function(oldIdentifier, newIdentifier, oldSelector, newSelector)
    400     {
    401         // TODO: Implement Shifting the oldSelector, and its contents to a newSelector
    402     },
    403 
    404     get rootDOMNode()
    405     {
    406         return this.treeOutline.rootDOMNode;
    407     },
    408 
    409     set rootDOMNode(x)
    410     {
    411         this.treeOutline.rootDOMNode = x;
    412     },
    413 
    414     get focusedDOMNode()
    415     {
    416         return this.treeOutline.focusedDOMNode;
    417     },
    418 
    419     set focusedDOMNode(x)
    420     {
    421         this.treeOutline.focusedDOMNode = x;
    422     },
    423 
    424     _attributesUpdated: function(event)
    425     {
    426         this.recentlyModifiedNodes.push({node: event.data, updated: true});
    427         if (this.visible)
    428             this._updateModifiedNodesSoon();
    429 
    430         if (!this.sidebarPanes.styles.isModifyingStyle && event.data === this.focusedDOMNode)
    431             this._styleSheetChanged();
    432     },
    433 
    434     _characterDataModified: function(event)
    435     {
    436         this.recentlyModifiedNodes.push({node: event.data, updated: true});
    437         if (this.visible)
    438             this._updateModifiedNodesSoon();
    439     },
    440 
    441     _nodeInserted: function(event)
    442     {
    443         this.recentlyModifiedNodes.push({node: event.data, parent: event.data.parentNode, inserted: true});
    444         if (this.visible)
    445             this._updateModifiedNodesSoon();
    446     },
    447 
    448     _nodeRemoved: function(event)
    449     {
    450         this.recentlyModifiedNodes.push({node: event.data.node, parent: event.data.parent, removed: true});
    451         if (this.visible)
    452             this._updateModifiedNodesSoon();
    453     },
    454 
    455     _childNodeCountUpdated: function(event)
    456     {
    457         var treeElement = this.treeOutline.findTreeElement(event.data);
    458         if (treeElement)
    459             treeElement.hasChildren = event.data.hasChildNodes();
    460     },
    461 
    462     _updateModifiedNodesSoon: function()
    463     {
    464         if ("_updateModifiedNodesTimeout" in this)
    465             return;
    466         this._updateModifiedNodesTimeout = setTimeout(this.updateModifiedNodes.bind(this), 0);
    467     },
    468 
    469     updateModifiedNodes: function()
    470     {
    471         if ("_updateModifiedNodesTimeout" in this) {
    472             clearTimeout(this._updateModifiedNodesTimeout);
    473             delete this._updateModifiedNodesTimeout;
    474         }
    475 
    476         var updatedParentTreeElements = [];
    477         var updateBreadcrumbs = false;
    478 
    479         for (var i = 0; i < this.recentlyModifiedNodes.length; ++i) {
    480             var parent = this.recentlyModifiedNodes[i].parent;
    481             var node = this.recentlyModifiedNodes[i].node;
    482 
    483             if (this.recentlyModifiedNodes[i].updated) {
    484                 var nodeItem = this.treeOutline.findTreeElement(node);
    485                 if (nodeItem)
    486                     nodeItem.updateTitle();
    487                 continue;
    488             }
    489 
    490             if (!parent)
    491                 continue;
    492 
    493             var parentNodeItem = this.treeOutline.findTreeElement(parent);
    494             if (parentNodeItem && !parentNodeItem.alreadyUpdatedChildren) {
    495                 parentNodeItem.updateChildren();
    496                 parentNodeItem.alreadyUpdatedChildren = true;
    497                 updatedParentTreeElements.push(parentNodeItem);
    498             }
    499 
    500             if (!updateBreadcrumbs && (this.focusedDOMNode === parent || isAncestorNode(this.focusedDOMNode, parent)))
    501                 updateBreadcrumbs = true;
    502         }
    503 
    504         for (var i = 0; i < updatedParentTreeElements.length; ++i)
    505             delete updatedParentTreeElements[i].alreadyUpdatedChildren;
    506 
    507         this.recentlyModifiedNodes = [];
    508 
    509         if (updateBreadcrumbs)
    510             this.updateBreadcrumb(true);
    511     },
    512 
    513     _stylesPaneEdited: function()
    514     {
    515         // Once styles are edited, the Metrics pane should be updated.
    516         this.sidebarPanes.metrics.needsUpdate = true;
    517         this.updateMetrics();
    518     },
    519 
    520     _metricsPaneEdited: function()
    521     {
    522         // Once metrics are edited, the Styles pane should be updated.
    523         this.sidebarPanes.styles.needsUpdate = true;
    524         this.updateStyles(true);
    525     },
    526 
    527     _styleSheetChanged: function()
    528     {
    529         this._metricsPaneEdited();
    530         this._stylesPaneEdited();
    531     },
    532 
    533     _mouseMovedInCrumbs: function(event)
    534     {
    535         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
    536         var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb");
    537 
    538         WebInspector.highlightDOMNode(crumbElement ? crumbElement.representedObject.id : 0);
    539 
    540         if ("_mouseOutOfCrumbsTimeout" in this) {
    541             clearTimeout(this._mouseOutOfCrumbsTimeout);
    542             delete this._mouseOutOfCrumbsTimeout;
    543         }
    544     },
    545 
    546     _mouseMovedOutOfCrumbs: function(event)
    547     {
    548         var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY);
    549         if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.crumbsElement))
    550             return;
    551 
    552         WebInspector.highlightDOMNode(0);
    553 
    554         this._mouseOutOfCrumbsTimeout = setTimeout(this.updateBreadcrumbSizes.bind(this), 1000);
    555     },
    556 
    557     updateBreadcrumb: function(forceUpdate)
    558     {
    559         if (!this.visible)
    560             return;
    561 
    562         var crumbs = this.crumbsElement;
    563 
    564         var handled = false;
    565         var foundRoot = false;
    566         var crumb = crumbs.firstChild;
    567         while (crumb) {
    568             if (crumb.representedObject === this.rootDOMNode)
    569                 foundRoot = true;
    570 
    571             if (foundRoot)
    572                 crumb.addStyleClass("dimmed");
    573             else
    574                 crumb.removeStyleClass("dimmed");
    575 
    576             if (crumb.representedObject === this.focusedDOMNode) {
    577                 crumb.addStyleClass("selected");
    578                 handled = true;
    579             } else {
    580                 crumb.removeStyleClass("selected");
    581             }
    582 
    583             crumb = crumb.nextSibling;
    584         }
    585 
    586         if (handled && !forceUpdate) {
    587             // We don't need to rebuild the crumbs, but we need to adjust sizes
    588             // to reflect the new focused or root node.
    589             this.updateBreadcrumbSizes();
    590             return;
    591         }
    592 
    593         crumbs.removeChildren();
    594 
    595         var panel = this;
    596 
    597         function selectCrumbFunction(event)
    598         {
    599             var crumb = event.currentTarget;
    600             if (crumb.hasStyleClass("collapsed")) {
    601                 // Clicking a collapsed crumb will expose the hidden crumbs.
    602                 if (crumb === panel.crumbsElement.firstChild) {
    603                     // If the focused crumb is the first child, pick the farthest crumb
    604                     // that is still hidden. This allows the user to expose every crumb.
    605                     var currentCrumb = crumb;
    606                     while (currentCrumb) {
    607                         var hidden = currentCrumb.hasStyleClass("hidden");
    608                         var collapsed = currentCrumb.hasStyleClass("collapsed");
    609                         if (!hidden && !collapsed)
    610                             break;
    611                         crumb = currentCrumb;
    612                         currentCrumb = currentCrumb.nextSibling;
    613                     }
    614                 }
    615 
    616                 panel.updateBreadcrumbSizes(crumb);
    617             } else {
    618                 // Clicking a dimmed crumb or double clicking (event.detail >= 2)
    619                 // will change the root node in addition to the focused node.
    620                 if (event.detail >= 2 || crumb.hasStyleClass("dimmed"))
    621                     panel.rootDOMNode = crumb.representedObject.parentNode;
    622                 panel.focusedDOMNode = crumb.representedObject;
    623             }
    624 
    625             event.preventDefault();
    626         }
    627 
    628         foundRoot = false;
    629         for (var current = this.focusedDOMNode; current; current = current.parentNode) {
    630             if (current.nodeType() === Node.DOCUMENT_NODE)
    631                 continue;
    632 
    633             if (current === this.rootDOMNode)
    634                 foundRoot = true;
    635 
    636             var crumb = document.createElement("span");
    637             crumb.className = "crumb";
    638             crumb.representedObject = current;
    639             crumb.addEventListener("mousedown", selectCrumbFunction, false);
    640 
    641             var crumbTitle;
    642             switch (current.nodeType()) {
    643                 case Node.ELEMENT_NODE:
    644                     this.decorateNodeLabel(current, crumb);
    645                     break;
    646 
    647                 case Node.TEXT_NODE:
    648                     if (isNodeWhitespace.call(current))
    649                         crumbTitle = WebInspector.UIString("(whitespace)");
    650                     else
    651                         crumbTitle = WebInspector.UIString("(text)");
    652                     break
    653 
    654                 case Node.COMMENT_NODE:
    655                     crumbTitle = "<!-->";
    656                     break;
    657 
    658                 case Node.DOCUMENT_TYPE_NODE:
    659                     crumbTitle = "<!DOCTYPE>";
    660                     break;
    661 
    662                 default:
    663                     crumbTitle = this.treeOutline.nodeNameToCorrectCase(current.nodeName());
    664             }
    665 
    666             if (!crumb.childNodes.length) {
    667                 var nameElement = document.createElement("span");
    668                 nameElement.textContent = crumbTitle;
    669                 crumb.appendChild(nameElement);
    670                 crumb.title = crumbTitle;
    671             }
    672 
    673             if (foundRoot)
    674                 crumb.addStyleClass("dimmed");
    675             if (current === this.focusedDOMNode)
    676                 crumb.addStyleClass("selected");
    677             if (!crumbs.childNodes.length)
    678                 crumb.addStyleClass("end");
    679 
    680             crumbs.appendChild(crumb);
    681         }
    682 
    683         if (crumbs.hasChildNodes())
    684             crumbs.lastChild.addStyleClass("start");
    685 
    686         this.updateBreadcrumbSizes();
    687     },
    688 
    689     decorateNodeLabel: function(node, parentElement)
    690     {
    691         var title = this.treeOutline.nodeNameToCorrectCase(node.nodeName());
    692 
    693         var nameElement = document.createElement("span");
    694         nameElement.textContent = title;
    695         parentElement.appendChild(nameElement);
    696 
    697         var idAttribute = node.getAttribute("id");
    698         if (idAttribute) {
    699             var idElement = document.createElement("span");
    700             parentElement.appendChild(idElement);
    701 
    702             var part = "#" + idAttribute;
    703             title += part;
    704             idElement.appendChild(document.createTextNode(part));
    705 
    706             // Mark the name as extra, since the ID is more important.
    707             nameElement.className = "extra";
    708         }
    709 
    710         var classAttribute = node.getAttribute("class");
    711         if (classAttribute) {
    712             var classes = classAttribute.split(/\s+/);
    713             var foundClasses = {};
    714 
    715             if (classes.length) {
    716                 var classesElement = document.createElement("span");
    717                 classesElement.className = "extra";
    718                 parentElement.appendChild(classesElement);
    719 
    720                 for (var i = 0; i < classes.length; ++i) {
    721                     var className = classes[i];
    722                     if (className && !(className in foundClasses)) {
    723                         var part = "." + className;
    724                         title += part;
    725                         classesElement.appendChild(document.createTextNode(part));
    726                         foundClasses[className] = true;
    727                     }
    728                 }
    729             }
    730         }
    731         parentElement.title = title;
    732     },
    733 
    734     linkifyNodeReference: function(node)
    735     {
    736         var link = document.createElement("span");
    737         link.className = "node-link";
    738         this.decorateNodeLabel(node, link);
    739         WebInspector.wireElementWithDOMNode(link, node.id);
    740         return link;
    741     },
    742 
    743     linkifyNodeById: function(nodeId)
    744     {
    745         var node = WebInspector.domAgent.nodeForId(nodeId);
    746         if (!node)
    747             return document.createTextNode(WebInspector.UIString("<node>"));
    748         return this.linkifyNodeReference(node);
    749     },
    750 
    751     updateBreadcrumbSizes: function(focusedCrumb)
    752     {
    753         if (!this.visible)
    754             return;
    755 
    756         if (document.body.offsetWidth <= 0) {
    757             // The stylesheet hasn't loaded yet or the window is closed,
    758             // so we can't calculate what is need. Return early.
    759             return;
    760         }
    761 
    762         var crumbs = this.crumbsElement;
    763         if (!crumbs.childNodes.length || crumbs.offsetWidth <= 0)
    764             return; // No crumbs, do nothing.
    765 
    766         // A Zero index is the right most child crumb in the breadcrumb.
    767         var selectedIndex = 0;
    768         var focusedIndex = 0;
    769         var selectedCrumb;
    770 
    771         var i = 0;
    772         var crumb = crumbs.firstChild;
    773         while (crumb) {
    774             // Find the selected crumb and index.
    775             if (!selectedCrumb && crumb.hasStyleClass("selected")) {
    776                 selectedCrumb = crumb;
    777                 selectedIndex = i;
    778             }
    779 
    780             // Find the focused crumb index.
    781             if (crumb === focusedCrumb)
    782                 focusedIndex = i;
    783 
    784             // Remove any styles that affect size before
    785             // deciding to shorten any crumbs.
    786             if (crumb !== crumbs.lastChild)
    787                 crumb.removeStyleClass("start");
    788             if (crumb !== crumbs.firstChild)
    789                 crumb.removeStyleClass("end");
    790 
    791             crumb.removeStyleClass("compact");
    792             crumb.removeStyleClass("collapsed");
    793             crumb.removeStyleClass("hidden");
    794 
    795             crumb = crumb.nextSibling;
    796             ++i;
    797         }
    798 
    799         // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs().
    800         // The order of the crumbs in the document is opposite of the visual order.
    801         crumbs.firstChild.addStyleClass("end");
    802         crumbs.lastChild.addStyleClass("start");
    803 
    804         function crumbsAreSmallerThanContainer()
    805         {
    806             var rightPadding = 20;
    807             var errorWarningElement = document.getElementById("error-warning-count");
    808             if (!WebInspector.drawer.visible && errorWarningElement)
    809                 rightPadding += errorWarningElement.offsetWidth;
    810             return ((crumbs.totalOffsetLeft + crumbs.offsetWidth + rightPadding) < window.innerWidth);
    811         }
    812 
    813         if (crumbsAreSmallerThanContainer())
    814             return; // No need to compact the crumbs, they all fit at full size.
    815 
    816         var BothSides = 0;
    817         var AncestorSide = -1;
    818         var ChildSide = 1;
    819 
    820         function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb)
    821         {
    822             if (!significantCrumb)
    823                 significantCrumb = (focusedCrumb || selectedCrumb);
    824 
    825             if (significantCrumb === selectedCrumb)
    826                 var significantIndex = selectedIndex;
    827             else if (significantCrumb === focusedCrumb)
    828                 var significantIndex = focusedIndex;
    829             else {
    830                 var significantIndex = 0;
    831                 for (var i = 0; i < crumbs.childNodes.length; ++i) {
    832                     if (crumbs.childNodes[i] === significantCrumb) {
    833                         significantIndex = i;
    834                         break;
    835                     }
    836                 }
    837             }
    838 
    839             function shrinkCrumbAtIndex(index)
    840             {
    841                 var shrinkCrumb = crumbs.childNodes[index];
    842                 if (shrinkCrumb && shrinkCrumb !== significantCrumb)
    843                     shrinkingFunction(shrinkCrumb);
    844                 if (crumbsAreSmallerThanContainer())
    845                     return true; // No need to compact the crumbs more.
    846                 return false;
    847             }
    848 
    849             // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs
    850             // fit in the container or we run out of crumbs to shrink.
    851             if (direction) {
    852                 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb.
    853                 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1);
    854                 while (index !== significantIndex) {
    855                     if (shrinkCrumbAtIndex(index))
    856                         return true;
    857                     index += (direction > 0 ? 1 : -1);
    858                 }
    859             } else {
    860                 // Crumbs are shrunk in order of descending distance from the signifcant crumb,
    861                 // with a tie going to child crumbs.
    862                 var startIndex = 0;
    863                 var endIndex = crumbs.childNodes.length - 1;
    864                 while (startIndex != significantIndex || endIndex != significantIndex) {
    865                     var startDistance = significantIndex - startIndex;
    866                     var endDistance = endIndex - significantIndex;
    867                     if (startDistance >= endDistance)
    868                         var index = startIndex++;
    869                     else
    870                         var index = endIndex--;
    871                     if (shrinkCrumbAtIndex(index))
    872                         return true;
    873                 }
    874             }
    875 
    876             // We are not small enough yet, return false so the caller knows.
    877             return false;
    878         }
    879 
    880         function coalesceCollapsedCrumbs()
    881         {
    882             var crumb = crumbs.firstChild;
    883             var collapsedRun = false;
    884             var newStartNeeded = false;
    885             var newEndNeeded = false;
    886             while (crumb) {
    887                 var hidden = crumb.hasStyleClass("hidden");
    888                 if (!hidden) {
    889                     var collapsed = crumb.hasStyleClass("collapsed");
    890                     if (collapsedRun && collapsed) {
    891                         crumb.addStyleClass("hidden");
    892                         crumb.removeStyleClass("compact");
    893                         crumb.removeStyleClass("collapsed");
    894 
    895                         if (crumb.hasStyleClass("start")) {
    896                             crumb.removeStyleClass("start");
    897                             newStartNeeded = true;
    898                         }
    899 
    900                         if (crumb.hasStyleClass("end")) {
    901                             crumb.removeStyleClass("end");
    902                             newEndNeeded = true;
    903                         }
    904 
    905                         continue;
    906                     }
    907 
    908                     collapsedRun = collapsed;
    909 
    910                     if (newEndNeeded) {
    911                         newEndNeeded = false;
    912                         crumb.addStyleClass("end");
    913                     }
    914                 } else
    915                     collapsedRun = true;
    916                 crumb = crumb.nextSibling;
    917             }
    918 
    919             if (newStartNeeded) {
    920                 crumb = crumbs.lastChild;
    921                 while (crumb) {
    922                     if (!crumb.hasStyleClass("hidden")) {
    923                         crumb.addStyleClass("start");
    924                         break;
    925                     }
    926                     crumb = crumb.previousSibling;
    927                 }
    928             }
    929         }
    930 
    931         function compact(crumb)
    932         {
    933             if (crumb.hasStyleClass("hidden"))
    934                 return;
    935             crumb.addStyleClass("compact");
    936         }
    937 
    938         function collapse(crumb, dontCoalesce)
    939         {
    940             if (crumb.hasStyleClass("hidden"))
    941                 return;
    942             crumb.addStyleClass("collapsed");
    943             crumb.removeStyleClass("compact");
    944             if (!dontCoalesce)
    945                 coalesceCollapsedCrumbs();
    946         }
    947 
    948         function compactDimmed(crumb)
    949         {
    950             if (crumb.hasStyleClass("dimmed"))
    951                 compact(crumb);
    952         }
    953 
    954         function collapseDimmed(crumb)
    955         {
    956             if (crumb.hasStyleClass("dimmed"))
    957                 collapse(crumb);
    958         }
    959 
    960         if (!focusedCrumb) {
    961             // When not focused on a crumb we can be biased and collapse less important
    962             // crumbs that the user might not care much about.
    963 
    964             // Compact child crumbs.
    965             if (makeCrumbsSmaller(compact, ChildSide))
    966                 return;
    967 
    968             // Collapse child crumbs.
    969             if (makeCrumbsSmaller(collapse, ChildSide))
    970                 return;
    971 
    972             // Compact dimmed ancestor crumbs.
    973             if (makeCrumbsSmaller(compactDimmed, AncestorSide))
    974                 return;
    975 
    976             // Collapse dimmed ancestor crumbs.
    977             if (makeCrumbsSmaller(collapseDimmed, AncestorSide))
    978                 return;
    979         }
    980 
    981         // Compact ancestor crumbs, or from both sides if focused.
    982         if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide)))
    983             return;
    984 
    985         // Collapse ancestor crumbs, or from both sides if focused.
    986         if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide)))
    987             return;
    988 
    989         if (!selectedCrumb)
    990             return;
    991 
    992         // Compact the selected crumb.
    993         compact(selectedCrumb);
    994         if (crumbsAreSmallerThanContainer())
    995             return;
    996 
    997         // Collapse the selected crumb as a last resort. Pass true to prevent coalescing.
    998         collapse(selectedCrumb, true);
    999     },
   1000 
   1001     updateStyles: function(forceUpdate)
   1002     {
   1003         var stylesSidebarPane = this.sidebarPanes.styles;
   1004         var computedStylePane = this.sidebarPanes.computedStyle;
   1005         if ((!stylesSidebarPane.expanded && !computedStylePane.expanded) || !stylesSidebarPane.needsUpdate)
   1006             return;
   1007 
   1008         stylesSidebarPane.update(this.focusedDOMNode, null, forceUpdate);
   1009         stylesSidebarPane.needsUpdate = false;
   1010     },
   1011 
   1012     updateMetrics: function()
   1013     {
   1014         var metricsSidebarPane = this.sidebarPanes.metrics;
   1015         if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate)
   1016             return;
   1017 
   1018         metricsSidebarPane.update(this.focusedDOMNode);
   1019         metricsSidebarPane.needsUpdate = false;
   1020     },
   1021 
   1022     updateProperties: function()
   1023     {
   1024         var propertiesSidebarPane = this.sidebarPanes.properties;
   1025         if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate)
   1026             return;
   1027 
   1028         propertiesSidebarPane.update(this.focusedDOMNode);
   1029         propertiesSidebarPane.needsUpdate = false;
   1030     },
   1031 
   1032     updateEventListeners: function()
   1033     {
   1034         var eventListenersSidebarPane = this.sidebarPanes.eventListeners;
   1035         if (!eventListenersSidebarPane.expanded || !eventListenersSidebarPane.needsUpdate)
   1036             return;
   1037 
   1038         eventListenersSidebarPane.update(this.focusedDOMNode);
   1039         eventListenersSidebarPane.needsUpdate = false;
   1040     },
   1041 
   1042     _registerShortcuts: function()
   1043     {
   1044         var shortcut = WebInspector.KeyboardShortcut;
   1045         var section = WebInspector.shortcutsHelp.section(WebInspector.UIString("Elements Panel"));
   1046         var keys = [
   1047             shortcut.shortcutToString(shortcut.Keys.Up),
   1048             shortcut.shortcutToString(shortcut.Keys.Down)
   1049         ];
   1050         section.addRelatedKeys(keys, WebInspector.UIString("Navigate elements"));
   1051         var keys = [
   1052             shortcut.shortcutToString(shortcut.Keys.Right),
   1053             shortcut.shortcutToString(shortcut.Keys.Left)
   1054         ];
   1055         section.addRelatedKeys(keys, WebInspector.UIString("Expand/collapse"));
   1056         section.addKey(shortcut.shortcutToString(shortcut.Keys.Enter), WebInspector.UIString("Edit attribute"));
   1057 
   1058         this.sidebarPanes.styles.registerShortcuts();
   1059     },
   1060 
   1061     handleShortcut: function(event)
   1062     {
   1063         // Cmd/Control + Shift + C should be a shortcut to clicking the Node Search Button.
   1064         // This shortcut matches Firebug.
   1065         if (event.keyIdentifier === "U+0043") {     // C key
   1066             if (WebInspector.isMac())
   1067                 var isNodeSearchKey = event.metaKey && !event.ctrlKey && !event.altKey && event.shiftKey;
   1068             else
   1069                 var isNodeSearchKey = event.ctrlKey && !event.metaKey && !event.altKey && event.shiftKey;
   1070 
   1071             if (isNodeSearchKey) {
   1072                 this.toggleSearchingForNode();
   1073                 event.handled = true;
   1074                 return;
   1075             }
   1076         }
   1077     },
   1078 
   1079     handleCopyEvent: function(event)
   1080     {
   1081         // Don't prevent the normal copy if the user has a selection.
   1082         if (!window.getSelection().isCollapsed)
   1083             return;
   1084         event.clipboardData.clearData();
   1085         event.preventDefault();
   1086         this.focusedDOMNode.copyNode();
   1087     },
   1088 
   1089     rightSidebarResizerDragStart: function(event)
   1090     {
   1091         WebInspector.elementDragStart(this.sidebarElement, this.rightSidebarResizerDrag.bind(this), this.rightSidebarResizerDragEnd.bind(this), event, "col-resize");
   1092     },
   1093 
   1094     rightSidebarResizerDragEnd: function(event)
   1095     {
   1096         WebInspector.elementDragEnd(event);
   1097         this.saveSidebarWidth();
   1098     },
   1099 
   1100     rightSidebarResizerDrag: function(event)
   1101     {
   1102         var x = event.pageX;
   1103         var newWidth = Number.constrain(window.innerWidth - x, Preferences.minElementsSidebarWidth, window.innerWidth * 0.66);
   1104         this.setSidebarWidth(newWidth);
   1105         event.preventDefault();
   1106     },
   1107 
   1108     setSidebarWidth: function(newWidth)
   1109     {
   1110         this.sidebarElement.style.width = newWidth + "px";
   1111         this.contentElement.style.right = newWidth + "px";
   1112         this.sidebarResizeElement.style.right = (newWidth - 3) + "px";
   1113         this.treeOutline.updateSelection();
   1114     },
   1115 
   1116     updateFocusedNode: function(nodeId)
   1117     {
   1118         var node = WebInspector.domAgent.nodeForId(nodeId);
   1119         if (!node)
   1120             return;
   1121 
   1122         this.focusedDOMNode = node;
   1123         this._nodeSearchButton.toggled = false;
   1124     },
   1125 
   1126     _setSearchingForNode: function(enabled)
   1127     {
   1128         this._nodeSearchButton.toggled = enabled;
   1129     },
   1130 
   1131     setSearchingForNode: function(enabled)
   1132     {
   1133         DOMAgent.setSearchingForNode(enabled, this._setSearchingForNode.bind(this, enabled));
   1134     },
   1135 
   1136     toggleSearchingForNode: function()
   1137     {
   1138         this.setSearchingForNode(!this._nodeSearchButton.toggled);
   1139     },
   1140 
   1141     elementsToRestoreScrollPositionsFor: function()
   1142     {
   1143         return [ this.contentElement, this.sidebarElement ];
   1144     }
   1145 }
   1146 
   1147 WebInspector.ElementsPanel.prototype.__proto__ = WebInspector.Panel.prototype;
   1148