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