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); 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 (InspectorBackend.searchingForNode()) { 62 InspectorBackend.toggleNodeSearch(); 63 this.panel.nodeSearchButton.toggled = false; 64 } 65 if (this._focusedDOMNode) 66 InjectedScriptAccess.get(this._focusedDOMNode.injectedScriptId).addInspectedNode(this._focusedDOMNode.id, function() {}); 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.styles = new WebInspector.StylesSidebarPane(); 78 this.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane(); 79 this.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane(); 80 this.sidebarPanes.eventListeners = new WebInspector.EventListenersSidebarPane(); 81 82 this.sidebarPanes.styles.onexpand = this.updateStyles.bind(this); 83 this.sidebarPanes.metrics.onexpand = this.updateMetrics.bind(this); 84 this.sidebarPanes.properties.onexpand = this.updateProperties.bind(this); 85 this.sidebarPanes.eventListeners.onexpand = this.updateEventListeners.bind(this); 86 87 this.sidebarPanes.styles.expanded = true; 88 89 this.sidebarPanes.styles.addEventListener("style edited", this._stylesPaneEdited, this); 90 this.sidebarPanes.styles.addEventListener("style property toggled", this._stylesPaneEdited, this); 91 this.sidebarPanes.metrics.addEventListener("metrics edited", this._metricsPaneEdited, this); 92 93 this.sidebarElement = document.createElement("div"); 94 this.sidebarElement.id = "elements-sidebar"; 95 96 this.sidebarElement.appendChild(this.sidebarPanes.styles.element); 97 this.sidebarElement.appendChild(this.sidebarPanes.metrics.element); 98 this.sidebarElement.appendChild(this.sidebarPanes.properties.element); 99 this.sidebarElement.appendChild(this.sidebarPanes.eventListeners.element); 100 101 this.sidebarResizeElement = document.createElement("div"); 102 this.sidebarResizeElement.className = "sidebar-resizer-vertical"; 103 this.sidebarResizeElement.addEventListener("mousedown", this.rightSidebarResizerDragStart.bind(this), false); 104 105 this.nodeSearchButton = new WebInspector.StatusBarButton(WebInspector.UIString("Select an element in the page to inspect it."), "node-search-status-bar-item"); 106 this.nodeSearchButton.addEventListener("click", this._nodeSearchButtonClicked.bind(this), false); 107 108 this.searchingForNode = false; 109 110 this.element.appendChild(this.contentElement); 111 this.element.appendChild(this.sidebarElement); 112 this.element.appendChild(this.sidebarResizeElement); 113 114 this._changedStyles = {}; 115 116 this.reset(); 117 } 118 119 WebInspector.ElementsPanel.prototype = { 120 toolbarItemClass: "elements", 121 122 get toolbarItemLabel() 123 { 124 return WebInspector.UIString("Elements"); 125 }, 126 127 get statusBarItems() 128 { 129 return [this.nodeSearchButton.element, this.crumbsElement]; 130 }, 131 132 get defaultFocusedElement() 133 { 134 return this.treeOutline.element; 135 }, 136 137 updateStatusBarItems: function() 138 { 139 this.updateBreadcrumbSizes(); 140 }, 141 142 show: function() 143 { 144 WebInspector.Panel.prototype.show.call(this); 145 this.sidebarResizeElement.style.right = (this.sidebarElement.offsetWidth - 3) + "px"; 146 this.updateBreadcrumb(); 147 this.treeOutline.updateSelection(); 148 if (this.recentlyModifiedNodes.length) 149 this.updateModifiedNodes(); 150 }, 151 152 hide: function() 153 { 154 WebInspector.Panel.prototype.hide.call(this); 155 156 WebInspector.hoveredDOMNode = null; 157 158 if (InspectorBackend.searchingForNode()) { 159 InspectorBackend.toggleNodeSearch(); 160 this.nodeSearchButton.toggled = false; 161 } 162 }, 163 164 resize: function() 165 { 166 this.treeOutline.updateSelection(); 167 this.updateBreadcrumbSizes(); 168 }, 169 170 reset: function() 171 { 172 if (this.focusedDOMNode) { 173 this._selectedPathOnReset = []; 174 var node = this.focusedDOMNode; 175 while ("index" in node) { 176 this._selectedPathOnReset.push(node.nodeName); 177 this._selectedPathOnReset.push(node.index); 178 node = node.parentNode; 179 } 180 this._selectedPathOnReset.reverse(); 181 } 182 183 this.rootDOMNode = null; 184 this.focusedDOMNode = null; 185 186 WebInspector.hoveredDOMNode = null; 187 188 if (InspectorBackend.searchingForNode()) { 189 InspectorBackend.toggleNodeSearch(); 190 this.nodeSearchButton.toggled = false; 191 } 192 193 this.recentlyModifiedNodes = []; 194 195 delete this.currentQuery; 196 this.searchCanceled(); 197 }, 198 199 setDocument: function(inspectedRootDocument) 200 { 201 this.reset(); 202 203 if (!inspectedRootDocument) 204 return; 205 206 inspectedRootDocument.addEventListener("DOMNodeInserted", this._nodeInserted.bind(this)); 207 inspectedRootDocument.addEventListener("DOMNodeRemoved", this._nodeRemoved.bind(this)); 208 inspectedRootDocument.addEventListener("DOMAttrModified", this._attributesUpdated.bind(this)); 209 210 this.treeOutline.suppressSelectHighlight = true; 211 this.rootDOMNode = inspectedRootDocument; 212 this.treeOutline.suppressSelectHighlight = false; 213 214 function selectNode(candidateFocusNode) 215 { 216 if (!candidateFocusNode) 217 candidateFocusNode = inspectedRootDocument.body || inspectedRootDocument.documentElement; 218 219 if (!candidateFocusNode) 220 return; 221 222 this.treeOutline.suppressSelectHighlight = true; 223 this.focusedDOMNode = candidateFocusNode; 224 if (this.treeOutline.selectedTreeElement) 225 this.treeOutline.selectedTreeElement.expand(); 226 this.treeOutline.suppressSelectHighlight = false; 227 } 228 229 function selectLastSelectedNode(nodeId) 230 { 231 if (this.focusedDOMNode) { 232 // Focused node has been explicitly set while reaching out for the last selected node. 233 return; 234 } 235 var node = nodeId ? WebInspector.domAgent.nodeForId(nodeId) : 0; 236 selectNode.call(this, node); 237 } 238 239 if (this._selectedPathOnReset) 240 InjectedScriptAccess.getDefault().nodeByPath(this._selectedPathOnReset, selectLastSelectedNode.bind(this)); 241 else 242 selectNode.call(this); 243 delete this._selectedPathOnReset; 244 }, 245 246 searchCanceled: function() 247 { 248 if (this._searchResults) { 249 for (var i = 0; i < this._searchResults.length; ++i) { 250 var treeElement = this.treeOutline.findTreeElement(this._searchResults[i]); 251 if (treeElement) 252 treeElement.highlighted = false; 253 } 254 } 255 256 WebInspector.updateSearchMatchesCount(0, this); 257 258 this._currentSearchResultIndex = 0; 259 this._searchResults = []; 260 InjectedScriptAccess.getDefault().searchCanceled(function() {}); 261 }, 262 263 performSearch: function(query) 264 { 265 // Call searchCanceled since it will reset everything we need before doing a new search. 266 this.searchCanceled(); 267 268 const whitespaceTrimmedQuery = query.trim(); 269 if (!whitespaceTrimmedQuery.length) 270 return; 271 272 this._updatedMatchCountOnce = false; 273 this._matchesCountUpdateTimeout = null; 274 275 InjectedScriptAccess.getDefault().performSearch(whitespaceTrimmedQuery, function() {}); 276 }, 277 278 _updateMatchesCount: function() 279 { 280 WebInspector.updateSearchMatchesCount(this._searchResults.length, this); 281 this._matchesCountUpdateTimeout = null; 282 this._updatedMatchCountOnce = true; 283 }, 284 285 _updateMatchesCountSoon: function() 286 { 287 if (!this._updatedMatchCountOnce) 288 return this._updateMatchesCount(); 289 if (this._matchesCountUpdateTimeout) 290 return; 291 // Update the matches count every half-second so it doesn't feel twitchy. 292 this._matchesCountUpdateTimeout = setTimeout(this._updateMatchesCount.bind(this), 500); 293 }, 294 295 addNodesToSearchResult: function(nodeIds) 296 { 297 if (!nodeIds) 298 return; 299 300 var nodeIdsArray = nodeIds.split(","); 301 for (var i = 0; i < nodeIdsArray.length; ++i) { 302 var nodeId = nodeIdsArray[i]; 303 var node = WebInspector.domAgent.nodeForId(nodeId); 304 if (!node) 305 continue; 306 307 if (!this._searchResults.length) { 308 this._currentSearchResultIndex = 0; 309 310 // Only change the focusedDOMNode if the search was manually performed, because 311 // the search may have been performed programmatically and we wouldn't want to 312 // change the current focusedDOMNode. 313 if (WebInspector.currentFocusElement === document.getElementById("search")) 314 this.focusedDOMNode = node; 315 } 316 317 this._searchResults.push(node); 318 319 // Highlight the tree element to show it matched the search. 320 // FIXME: highlight the substrings in text nodes and attributes. 321 var treeElement = this.treeOutline.findTreeElement(node); 322 if (treeElement) 323 treeElement.highlighted = true; 324 } 325 326 this._updateMatchesCountSoon(); 327 }, 328 329 jumpToNextSearchResult: function() 330 { 331 if (!this._searchResults || !this._searchResults.length) 332 return; 333 if (++this._currentSearchResultIndex >= this._searchResults.length) 334 this._currentSearchResultIndex = 0; 335 this.focusedDOMNode = this._searchResults[this._currentSearchResultIndex]; 336 }, 337 338 jumpToPreviousSearchResult: function() 339 { 340 if (!this._searchResults || !this._searchResults.length) 341 return; 342 if (--this._currentSearchResultIndex < 0) 343 this._currentSearchResultIndex = (this._searchResults.length - 1); 344 this.focusedDOMNode = this._searchResults[this._currentSearchResultIndex]; 345 }, 346 347 renameSelector: function(oldIdentifier, newIdentifier, oldSelector, newSelector) 348 { 349 // TODO: Implement Shifting the oldSelector, and its contents to a newSelector 350 }, 351 352 addStyleChange: function(identifier, style, property) 353 { 354 if (!style.parentRule) 355 return; 356 357 var selector = style.parentRule.selectorText; 358 if (!this._changedStyles[identifier]) 359 this._changedStyles[identifier] = {}; 360 361 if (!this._changedStyles[identifier][selector]) 362 this._changedStyles[identifier][selector] = {}; 363 364 if (!this._changedStyles[identifier][selector][property]) 365 WebInspector.styleChanges += 1; 366 367 this._changedStyles[identifier][selector][property] = style.getPropertyValue(property); 368 }, 369 370 removeStyleChange: function(identifier, style, property) 371 { 372 if (!style.parentRule) 373 return; 374 375 var selector = style.parentRule.selectorText; 376 if (!this._changedStyles[identifier] || !this._changedStyles[identifier][selector]) 377 return; 378 379 if (this._changedStyles[identifier][selector][property]) { 380 delete this._changedStyles[identifier][selector][property]; 381 WebInspector.styleChanges -= 1; 382 } 383 }, 384 385 generateStylesheet: function() 386 { 387 if (!WebInspector.styleChanges) 388 return; 389 390 // Merge Down to Just Selectors 391 var mergedSelectors = {}; 392 for (var identifier in this._changedStyles) { 393 for (var selector in this._changedStyles[identifier]) { 394 if (!mergedSelectors[selector]) 395 mergedSelectors[selector] = this._changedStyles[identifier][selector]; 396 else { // merge on selector 397 var merge = {}; 398 for (var property in mergedSelectors[selector]) 399 merge[property] = mergedSelectors[selector][property]; 400 for (var property in this._changedStyles[identifier][selector]) { 401 if (!merge[property]) 402 merge[property] = this._changedStyles[identifier][selector][property]; 403 else { // merge on property within a selector, include comment to notify user 404 var value1 = merge[property]; 405 var value2 = this._changedStyles[identifier][selector][property]; 406 407 if (value1 === value2) 408 merge[property] = [value1]; 409 else if (value1 instanceof Array) 410 merge[property].push(value2); 411 else 412 merge[property] = [value1, value2]; 413 } 414 } 415 mergedSelectors[selector] = merge; 416 } 417 } 418 } 419 420 var builder = []; 421 builder.push("/**"); 422 builder.push(" * Inspector Generated Stylesheet"); // UIString? 423 builder.push(" */\n"); 424 425 var indent = " "; 426 function displayProperty(property, value, comment) { 427 if (comment) 428 return indent + "/* " + property + ": " + value + "; */"; 429 else 430 return indent + property + ": " + value + ";"; 431 } 432 433 for (var selector in mergedSelectors) { 434 var psuedoStyle = mergedSelectors[selector]; 435 var properties = Object.properties(psuedoStyle); 436 if (properties.length) { 437 builder.push(selector + " {"); 438 for (var i = 0; i < properties.length; ++i) { 439 var property = properties[i]; 440 var value = psuedoStyle[property]; 441 if (!(value instanceof Array)) 442 builder.push(displayProperty(property, value)); 443 else { 444 if (value.length === 1) 445 builder.push(displayProperty(property, value) + " /* merged from equivalent edits */"); // UIString? 446 else { 447 builder.push(indent + "/* There was a Conflict... There were Multiple Edits for '" + property + "' */"); // UIString? 448 for (var j = 0; j < value.length; ++j) 449 builder.push(displayProperty(property, value, true)); 450 } 451 } 452 } 453 builder.push("}\n"); 454 } 455 } 456 457 WebInspector.showConsole(); 458 WebInspector.console.addMessage(new WebInspector.ConsoleTextMessage(builder.join("\n"))); 459 }, 460 461 get rootDOMNode() 462 { 463 return this.treeOutline.rootDOMNode; 464 }, 465 466 set rootDOMNode(x) 467 { 468 this.treeOutline.rootDOMNode = x; 469 }, 470 471 get focusedDOMNode() 472 { 473 return this.treeOutline.focusedDOMNode; 474 }, 475 476 set focusedDOMNode(x) 477 { 478 this.treeOutline.focusedDOMNode = x; 479 }, 480 481 _attributesUpdated: function(event) 482 { 483 this.recentlyModifiedNodes.push({node: event.target, updated: true}); 484 if (this.visible) 485 this._updateModifiedNodesSoon(); 486 }, 487 488 _nodeInserted: function(event) 489 { 490 this.recentlyModifiedNodes.push({node: event.target, parent: event.relatedNode, inserted: true}); 491 if (this.visible) 492 this._updateModifiedNodesSoon(); 493 }, 494 495 _nodeRemoved: function(event) 496 { 497 this.recentlyModifiedNodes.push({node: event.target, parent: event.relatedNode, removed: true}); 498 if (this.visible) 499 this._updateModifiedNodesSoon(); 500 }, 501 502 _updateModifiedNodesSoon: function() 503 { 504 if ("_updateModifiedNodesTimeout" in this) 505 return; 506 this._updateModifiedNodesTimeout = setTimeout(this.updateModifiedNodes.bind(this), 0); 507 }, 508 509 updateModifiedNodes: function() 510 { 511 if ("_updateModifiedNodesTimeout" in this) { 512 clearTimeout(this._updateModifiedNodesTimeout); 513 delete this._updateModifiedNodesTimeout; 514 } 515 516 var updatedParentTreeElements = []; 517 var updateBreadcrumbs = false; 518 519 for (var i = 0; i < this.recentlyModifiedNodes.length; ++i) { 520 var replaced = this.recentlyModifiedNodes[i].replaced; 521 var parent = this.recentlyModifiedNodes[i].parent; 522 var node = this.recentlyModifiedNodes[i].node; 523 524 if (this.recentlyModifiedNodes[i].updated) { 525 var nodeItem = this.treeOutline.findTreeElement(node); 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 crumbTitle = current.nodeName.toLowerCase(); 677 678 var nameElement = document.createElement("span"); 679 nameElement.textContent = crumbTitle; 680 crumb.appendChild(nameElement); 681 682 var idAttribute = current.getAttribute("id"); 683 if (idAttribute) { 684 var idElement = document.createElement("span"); 685 crumb.appendChild(idElement); 686 687 var part = "#" + idAttribute; 688 crumbTitle += part; 689 idElement.appendChild(document.createTextNode(part)); 690 691 // Mark the name as extra, since the ID is more important. 692 nameElement.className = "extra"; 693 } 694 695 var classAttribute = current.getAttribute("class"); 696 if (classAttribute) { 697 var classes = classAttribute.split(/\s+/); 698 var foundClasses = {}; 699 700 if (classes.length) { 701 var classesElement = document.createElement("span"); 702 classesElement.className = "extra"; 703 crumb.appendChild(classesElement); 704 705 for (var i = 0; i < classes.length; ++i) { 706 var className = classes[i]; 707 if (className && !(className in foundClasses)) { 708 var part = "." + className; 709 crumbTitle += part; 710 classesElement.appendChild(document.createTextNode(part)); 711 foundClasses[className] = true; 712 } 713 } 714 } 715 } 716 717 break; 718 719 case Node.TEXT_NODE: 720 if (isNodeWhitespace.call(current)) 721 crumbTitle = WebInspector.UIString("(whitespace)"); 722 else 723 crumbTitle = WebInspector.UIString("(text)"); 724 break 725 726 case Node.COMMENT_NODE: 727 crumbTitle = "<!-->"; 728 break; 729 730 case Node.DOCUMENT_TYPE_NODE: 731 crumbTitle = "<!DOCTYPE>"; 732 break; 733 734 default: 735 crumbTitle = current.nodeName.toLowerCase(); 736 } 737 738 if (!crumb.childNodes.length) { 739 var nameElement = document.createElement("span"); 740 nameElement.textContent = crumbTitle; 741 crumb.appendChild(nameElement); 742 } 743 744 crumb.title = crumbTitle; 745 746 if (foundRoot) 747 crumb.addStyleClass("dimmed"); 748 if (current === this.focusedDOMNode) 749 crumb.addStyleClass("selected"); 750 if (!crumbs.childNodes.length) 751 crumb.addStyleClass("end"); 752 753 crumbs.appendChild(crumb); 754 } 755 756 if (crumbs.hasChildNodes()) 757 crumbs.lastChild.addStyleClass("start"); 758 759 this.updateBreadcrumbSizes(); 760 }, 761 762 updateBreadcrumbSizes: function(focusedCrumb) 763 { 764 if (!this.visible) 765 return; 766 767 if (document.body.offsetWidth <= 0) { 768 // The stylesheet hasn't loaded yet or the window is closed, 769 // so we can't calculate what is need. Return early. 770 return; 771 } 772 773 var crumbs = this.crumbsElement; 774 if (!crumbs.childNodes.length || crumbs.offsetWidth <= 0) 775 return; // No crumbs, do nothing. 776 777 // A Zero index is the right most child crumb in the breadcrumb. 778 var selectedIndex = 0; 779 var focusedIndex = 0; 780 var selectedCrumb; 781 782 var i = 0; 783 var crumb = crumbs.firstChild; 784 while (crumb) { 785 // Find the selected crumb and index. 786 if (!selectedCrumb && crumb.hasStyleClass("selected")) { 787 selectedCrumb = crumb; 788 selectedIndex = i; 789 } 790 791 // Find the focused crumb index. 792 if (crumb === focusedCrumb) 793 focusedIndex = i; 794 795 // Remove any styles that affect size before 796 // deciding to shorten any crumbs. 797 if (crumb !== crumbs.lastChild) 798 crumb.removeStyleClass("start"); 799 if (crumb !== crumbs.firstChild) 800 crumb.removeStyleClass("end"); 801 802 crumb.removeStyleClass("compact"); 803 crumb.removeStyleClass("collapsed"); 804 crumb.removeStyleClass("hidden"); 805 806 crumb = crumb.nextSibling; 807 ++i; 808 } 809 810 // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs(). 811 // The order of the crumbs in the document is opposite of the visual order. 812 crumbs.firstChild.addStyleClass("end"); 813 crumbs.lastChild.addStyleClass("start"); 814 815 function crumbsAreSmallerThanContainer() 816 { 817 var rightPadding = 20; 818 var errorWarningElement = document.getElementById("error-warning-count"); 819 if (!WebInspector.drawer.visible && errorWarningElement) 820 rightPadding += errorWarningElement.offsetWidth; 821 return ((crumbs.totalOffsetLeft + crumbs.offsetWidth + rightPadding) < window.innerWidth); 822 } 823 824 if (crumbsAreSmallerThanContainer()) 825 return; // No need to compact the crumbs, they all fit at full size. 826 827 var BothSides = 0; 828 var AncestorSide = -1; 829 var ChildSide = 1; 830 831 function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb) 832 { 833 if (!significantCrumb) 834 significantCrumb = (focusedCrumb || selectedCrumb); 835 836 if (significantCrumb === selectedCrumb) 837 var significantIndex = selectedIndex; 838 else if (significantCrumb === focusedCrumb) 839 var significantIndex = focusedIndex; 840 else { 841 var significantIndex = 0; 842 for (var i = 0; i < crumbs.childNodes.length; ++i) { 843 if (crumbs.childNodes[i] === significantCrumb) { 844 significantIndex = i; 845 break; 846 } 847 } 848 } 849 850 function shrinkCrumbAtIndex(index) 851 { 852 var shrinkCrumb = crumbs.childNodes[index]; 853 if (shrinkCrumb && shrinkCrumb !== significantCrumb) 854 shrinkingFunction(shrinkCrumb); 855 if (crumbsAreSmallerThanContainer()) 856 return true; // No need to compact the crumbs more. 857 return false; 858 } 859 860 // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs 861 // fit in the container or we run out of crumbs to shrink. 862 if (direction) { 863 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb. 864 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1); 865 while (index !== significantIndex) { 866 if (shrinkCrumbAtIndex(index)) 867 return true; 868 index += (direction > 0 ? 1 : -1); 869 } 870 } else { 871 // Crumbs are shrunk in order of descending distance from the signifcant crumb, 872 // with a tie going to child crumbs. 873 var startIndex = 0; 874 var endIndex = crumbs.childNodes.length - 1; 875 while (startIndex != significantIndex || endIndex != significantIndex) { 876 var startDistance = significantIndex - startIndex; 877 var endDistance = endIndex - significantIndex; 878 if (startDistance >= endDistance) 879 var index = startIndex++; 880 else 881 var index = endIndex--; 882 if (shrinkCrumbAtIndex(index)) 883 return true; 884 } 885 } 886 887 // We are not small enough yet, return false so the caller knows. 888 return false; 889 } 890 891 function coalesceCollapsedCrumbs() 892 { 893 var crumb = crumbs.firstChild; 894 var collapsedRun = false; 895 var newStartNeeded = false; 896 var newEndNeeded = false; 897 while (crumb) { 898 var hidden = crumb.hasStyleClass("hidden"); 899 if (!hidden) { 900 var collapsed = crumb.hasStyleClass("collapsed"); 901 if (collapsedRun && collapsed) { 902 crumb.addStyleClass("hidden"); 903 crumb.removeStyleClass("compact"); 904 crumb.removeStyleClass("collapsed"); 905 906 if (crumb.hasStyleClass("start")) { 907 crumb.removeStyleClass("start"); 908 newStartNeeded = true; 909 } 910 911 if (crumb.hasStyleClass("end")) { 912 crumb.removeStyleClass("end"); 913 newEndNeeded = true; 914 } 915 916 continue; 917 } 918 919 collapsedRun = collapsed; 920 921 if (newEndNeeded) { 922 newEndNeeded = false; 923 crumb.addStyleClass("end"); 924 } 925 } else 926 collapsedRun = true; 927 crumb = crumb.nextSibling; 928 } 929 930 if (newStartNeeded) { 931 crumb = crumbs.lastChild; 932 while (crumb) { 933 if (!crumb.hasStyleClass("hidden")) { 934 crumb.addStyleClass("start"); 935 break; 936 } 937 crumb = crumb.previousSibling; 938 } 939 } 940 } 941 942 function compact(crumb) 943 { 944 if (crumb.hasStyleClass("hidden")) 945 return; 946 crumb.addStyleClass("compact"); 947 } 948 949 function collapse(crumb, dontCoalesce) 950 { 951 if (crumb.hasStyleClass("hidden")) 952 return; 953 crumb.addStyleClass("collapsed"); 954 crumb.removeStyleClass("compact"); 955 if (!dontCoalesce) 956 coalesceCollapsedCrumbs(); 957 } 958 959 function compactDimmed(crumb) 960 { 961 if (crumb.hasStyleClass("dimmed")) 962 compact(crumb); 963 } 964 965 function collapseDimmed(crumb) 966 { 967 if (crumb.hasStyleClass("dimmed")) 968 collapse(crumb); 969 } 970 971 if (!focusedCrumb) { 972 // When not focused on a crumb we can be biased and collapse less important 973 // crumbs that the user might not care much about. 974 975 // Compact child crumbs. 976 if (makeCrumbsSmaller(compact, ChildSide)) 977 return; 978 979 // Collapse child crumbs. 980 if (makeCrumbsSmaller(collapse, ChildSide)) 981 return; 982 983 // Compact dimmed ancestor crumbs. 984 if (makeCrumbsSmaller(compactDimmed, AncestorSide)) 985 return; 986 987 // Collapse dimmed ancestor crumbs. 988 if (makeCrumbsSmaller(collapseDimmed, AncestorSide)) 989 return; 990 } 991 992 // Compact ancestor crumbs, or from both sides if focused. 993 if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide))) 994 return; 995 996 // Collapse ancestor crumbs, or from both sides if focused. 997 if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide))) 998 return; 999 1000 if (!selectedCrumb) 1001 return; 1002 1003 // Compact the selected crumb. 1004 compact(selectedCrumb); 1005 if (crumbsAreSmallerThanContainer()) 1006 return; 1007 1008 // Collapse the selected crumb as a last resort. Pass true to prevent coalescing. 1009 collapse(selectedCrumb, true); 1010 }, 1011 1012 updateStyles: function(forceUpdate) 1013 { 1014 var stylesSidebarPane = this.sidebarPanes.styles; 1015 if (!stylesSidebarPane.expanded || !stylesSidebarPane.needsUpdate) 1016 return; 1017 1018 stylesSidebarPane.update(this.focusedDOMNode, null, forceUpdate); 1019 stylesSidebarPane.needsUpdate = false; 1020 }, 1021 1022 updateMetrics: function() 1023 { 1024 var metricsSidebarPane = this.sidebarPanes.metrics; 1025 if (!metricsSidebarPane.expanded || !metricsSidebarPane.needsUpdate) 1026 return; 1027 1028 metricsSidebarPane.update(this.focusedDOMNode); 1029 metricsSidebarPane.needsUpdate = false; 1030 }, 1031 1032 updateProperties: function() 1033 { 1034 var propertiesSidebarPane = this.sidebarPanes.properties; 1035 if (!propertiesSidebarPane.expanded || !propertiesSidebarPane.needsUpdate) 1036 return; 1037 1038 propertiesSidebarPane.update(this.focusedDOMNode); 1039 propertiesSidebarPane.needsUpdate = false; 1040 }, 1041 1042 updateEventListeners: function() 1043 { 1044 var eventListenersSidebarPane = this.sidebarPanes.eventListeners; 1045 if (!eventListenersSidebarPane.expanded || !eventListenersSidebarPane.needsUpdate) 1046 return; 1047 1048 eventListenersSidebarPane.update(this.focusedDOMNode); 1049 eventListenersSidebarPane.needsUpdate = false; 1050 }, 1051 1052 handleShortcut: function(event) 1053 { 1054 // Cmd/Control + Shift + C should be a shortcut to clicking the Node Search Button. 1055 // This shortcut matches Firebug. 1056 if (event.keyIdentifier === "U+0043") { // C key 1057 if (WebInspector.isMac()) 1058 var isNodeSearchKey = event.metaKey && !event.ctrlKey && !event.altKey && event.shiftKey; 1059 else 1060 var isNodeSearchKey = event.ctrlKey && !event.metaKey && !event.altKey && event.shiftKey; 1061 1062 if (isNodeSearchKey) { 1063 this._nodeSearchButtonClicked(event); 1064 event.handled = true; 1065 return; 1066 } 1067 } 1068 }, 1069 1070 handleCopyEvent: function(event) 1071 { 1072 // Don't prevent the normal copy if the user has a selection. 1073 if (!window.getSelection().isCollapsed) 1074 return; 1075 event.clipboardData.clearData(); 1076 event.preventDefault(); 1077 InspectorBackend.copyNode(this.focusedDOMNode.id); 1078 }, 1079 1080 rightSidebarResizerDragStart: function(event) 1081 { 1082 WebInspector.elementDragStart(this.sidebarElement, this.rightSidebarResizerDrag.bind(this), this.rightSidebarResizerDragEnd.bind(this), event, "col-resize"); 1083 }, 1084 1085 rightSidebarResizerDragEnd: function(event) 1086 { 1087 WebInspector.elementDragEnd(event); 1088 }, 1089 1090 rightSidebarResizerDrag: function(event) 1091 { 1092 var x = event.pageX; 1093 var newWidth = Number.constrain(window.innerWidth - x, Preferences.minElementsSidebarWidth, window.innerWidth * 0.66); 1094 1095 this.sidebarElement.style.width = newWidth + "px"; 1096 this.contentElement.style.right = newWidth + "px"; 1097 this.sidebarResizeElement.style.right = (newWidth - 3) + "px"; 1098 1099 this.treeOutline.updateSelection(); 1100 1101 event.preventDefault(); 1102 }, 1103 1104 _nodeSearchButtonClicked: function(event) 1105 { 1106 InspectorBackend.toggleNodeSearch(); 1107 1108 this.nodeSearchButton.toggled = InspectorBackend.searchingForNode(); 1109 } 1110 } 1111 1112 WebInspector.ElementsPanel.prototype.__proto__ = WebInspector.Panel.prototype; 1113