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