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