home *** CD-ROM | disk | FTP | other *** search
/ Computer Active Guide 2009 July / CAG7.ISO / Internetas / SafariSetup.exe / AppleApplicationSupport.msi / WebKit.resources_inspector_ElementsPanel.js < prev    next >
Encoding:
Text File  |  2010-06-03  |  39.4 KB  |  1,132 lines

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