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 importScript("CSSNamedFlowCollectionsView.js"); 32 importScript("CSSNamedFlowView.js"); 33 importScript("EventListenersSidebarPane.js"); 34 importScript("MetricsSidebarPane.js"); 35 importScript("PropertiesSidebarPane.js"); 36 importScript("StylesSidebarPane.js"); 37 38 /** 39 * @constructor 40 * @extends {WebInspector.Panel} 41 */ 42 WebInspector.ElementsPanel = function() 43 { 44 WebInspector.Panel.call(this, "elements"); 45 this.registerRequiredCSS("breadcrumbList.css"); 46 this.registerRequiredCSS("elementsPanel.css"); 47 this.registerRequiredCSS("textPrompt.css"); 48 this.setHideOnDetach(); 49 50 const initialSidebarWidth = 325; 51 const minimumContentWidthPercent = 0.34; 52 const initialSidebarHeight = 325; 53 const minimumContentHeightPercent = 0.34; 54 this.createSidebarView(this.element, WebInspector.SidebarView.SidebarPosition.End, initialSidebarWidth, initialSidebarHeight); 55 this.splitView.setSidebarElementConstraints(Preferences.minElementsSidebarWidth, Preferences.minElementsSidebarHeight); 56 this.splitView.setMainElementConstraints(minimumContentWidthPercent, minimumContentHeightPercent); 57 58 this.contentElement = this.splitView.mainElement; 59 this.contentElement.id = "elements-content"; 60 this.contentElement.addStyleClass("outline-disclosure"); 61 this.contentElement.addStyleClass("source-code"); 62 if (!WebInspector.settings.domWordWrap.get()) 63 this.contentElement.classList.add("nowrap"); 64 WebInspector.settings.domWordWrap.addChangeListener(this._domWordWrapSettingChanged.bind(this)); 65 66 this.contentElement.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true); 67 this.splitView.sidebarElement.addEventListener("contextmenu", this._sidebarContextMenuEventFired.bind(this), false); 68 69 this.treeOutline = new WebInspector.ElementsTreeOutline(true, true, false, this._populateContextMenu.bind(this), this._setPseudoClassForNodeId.bind(this)); 70 this.treeOutline.wireToDomAgent(); 71 72 this.treeOutline.addEventListener(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedNodeChanged, this); 73 74 this.crumbsElement = document.createElement("div"); 75 this.crumbsElement.className = "crumbs"; 76 this.crumbsElement.addEventListener("mousemove", this._mouseMovedInCrumbs.bind(this), false); 77 this.crumbsElement.addEventListener("mouseout", this._mouseMovedOutOfCrumbs.bind(this), false); 78 79 this.sidebarPanes = {}; 80 this.sidebarPanes.computedStyle = new WebInspector.ComputedStyleSidebarPane(); 81 this.sidebarPanes.styles = new WebInspector.StylesSidebarPane(this.sidebarPanes.computedStyle, this._setPseudoClassForNodeId.bind(this)); 82 this.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane(); 83 this.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane(); 84 this.sidebarPanes.domBreakpoints = WebInspector.domBreakpointsSidebarPane.createProxy(this); 85 this.sidebarPanes.eventListeners = new WebInspector.EventListenersSidebarPane(); 86 87 this.sidebarPanes.styles.addEventListener(WebInspector.SidebarPane.EventTypes.wasShown, this.updateStyles.bind(this, false)); 88 this.sidebarPanes.metrics.addEventListener(WebInspector.SidebarPane.EventTypes.wasShown, this.updateMetrics.bind(this)); 89 this.sidebarPanes.properties.addEventListener(WebInspector.SidebarPane.EventTypes.wasShown, this.updateProperties.bind(this)); 90 this.sidebarPanes.eventListeners.addEventListener(WebInspector.SidebarPane.EventTypes.wasShown, this.updateEventListeners.bind(this)); 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 96 WebInspector.dockController.addEventListener(WebInspector.DockController.Events.DockSideChanged, this._dockSideChanged.bind(this)); 97 WebInspector.settings.splitVerticallyWhenDockedToRight.addChangeListener(this._dockSideChanged.bind(this)); 98 this._dockSideChanged(); 99 100 this._popoverHelper = new WebInspector.PopoverHelper(this.element, this._getPopoverAnchor.bind(this), this._showPopover.bind(this)); 101 this._popoverHelper.setTimeout(0); 102 103 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrModified, this._updateBreadcrumbIfNeeded, this); 104 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.AttrRemoved, this._updateBreadcrumbIfNeeded, this); 105 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.NodeRemoved, this._nodeRemoved, this); 106 WebInspector.domAgent.addEventListener(WebInspector.DOMAgent.Events.DocumentUpdated, this._documentUpdatedEvent, this); 107 WebInspector.settings.showShadowDOM.addChangeListener(this._showShadowDOMChanged.bind(this)); 108 109 if (WebInspector.domAgent.existingDocument()) 110 this._documentUpdated(WebInspector.domAgent.existingDocument()); 111 } 112 113 WebInspector.ElementsPanel.prototype = { 114 get statusBarItems() 115 { 116 return [this.crumbsElement]; 117 }, 118 119 defaultFocusedElement: function() 120 { 121 return this.treeOutline.element; 122 }, 123 124 statusBarResized: function() 125 { 126 this.updateBreadcrumbSizes(); 127 }, 128 129 wasShown: function() 130 { 131 // Attach heavy component lazily 132 if (this.treeOutline.element.parentElement !== this.contentElement) 133 this.contentElement.appendChild(this.treeOutline.element); 134 135 WebInspector.Panel.prototype.wasShown.call(this); 136 137 this.updateBreadcrumb(); 138 this.treeOutline.updateSelection(); 139 this.treeOutline.setVisible(true); 140 141 if (!this.treeOutline.rootDOMNode) 142 WebInspector.domAgent.requestDocument(); 143 }, 144 145 willHide: function() 146 { 147 WebInspector.domAgent.hideDOMNodeHighlight(); 148 this.treeOutline.setVisible(false); 149 this._popoverHelper.hidePopover(); 150 151 // Detach heavy component on hide 152 this.contentElement.removeChild(this.treeOutline.element); 153 154 WebInspector.Panel.prototype.willHide.call(this); 155 }, 156 157 onResize: function() 158 { 159 this.treeOutline.updateSelection(); 160 this.updateBreadcrumbSizes(); 161 }, 162 163 /** 164 * @param {DOMAgent.NodeId} nodeId 165 * @param {string} pseudoClass 166 * @param {boolean} enable 167 */ 168 _setPseudoClassForNodeId: function(nodeId, pseudoClass, enable) 169 { 170 var node = WebInspector.domAgent.nodeForId(nodeId); 171 if (!node) 172 return; 173 174 var pseudoClasses = node.getUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName); 175 if (enable) { 176 pseudoClasses = pseudoClasses || []; 177 if (pseudoClasses.indexOf(pseudoClass) >= 0) 178 return; 179 pseudoClasses.push(pseudoClass); 180 node.setUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName, pseudoClasses); 181 } else { 182 if (!pseudoClasses || pseudoClasses.indexOf(pseudoClass) < 0) 183 return; 184 pseudoClasses.remove(pseudoClass); 185 if (!pseudoClasses.length) 186 node.removeUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName); 187 } 188 189 this.treeOutline.updateOpenCloseTags(node); 190 WebInspector.cssModel.forcePseudoState(node.id, node.getUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName)); 191 this._metricsPaneEdited(); 192 this._stylesPaneEdited(); 193 194 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, { 195 action: WebInspector.UserMetrics.UserActionNames.ForcedElementState, 196 selector: node.appropriateSelectorFor(false), 197 enabled: enable, 198 state: pseudoClass 199 }); 200 }, 201 202 _selectedNodeChanged: function() 203 { 204 var selectedNode = this.selectedDOMNode(); 205 if (!selectedNode && this._lastValidSelectedNode) 206 this._selectedPathOnReset = this._lastValidSelectedNode.path(); 207 208 this.updateBreadcrumb(false); 209 210 this._updateSidebars(); 211 212 if (selectedNode) { 213 ConsoleAgent.addInspectedNode(selectedNode.id); 214 this._lastValidSelectedNode = selectedNode; 215 } 216 WebInspector.notifications.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged); 217 }, 218 219 _updateSidebars: function() 220 { 221 for (var pane in this.sidebarPanes) 222 this.sidebarPanes[pane].needsUpdate = true; 223 224 this.updateStyles(true); 225 this.updateMetrics(); 226 this.updateProperties(); 227 this.updateEventListeners(); 228 }, 229 230 _reset: function() 231 { 232 delete this.currentQuery; 233 }, 234 235 _documentUpdatedEvent: function(event) 236 { 237 this._documentUpdated(event.data); 238 }, 239 240 _documentUpdated: function(inspectedRootDocument) 241 { 242 this._reset(); 243 this.searchCanceled(); 244 245 this.treeOutline.rootDOMNode = inspectedRootDocument; 246 247 if (!inspectedRootDocument) { 248 if (this.isShowing()) 249 WebInspector.domAgent.requestDocument(); 250 return; 251 } 252 253 WebInspector.domBreakpointsSidebarPane.restoreBreakpoints(); 254 255 /** 256 * @this {WebInspector.ElementsPanel} 257 * @param {WebInspector.DOMNode=} candidateFocusNode 258 */ 259 function selectNode(candidateFocusNode) 260 { 261 if (!candidateFocusNode) 262 candidateFocusNode = inspectedRootDocument.body || inspectedRootDocument.documentElement; 263 264 if (!candidateFocusNode) 265 return; 266 267 this.selectDOMNode(candidateFocusNode); 268 if (this.treeOutline.selectedTreeElement) 269 this.treeOutline.selectedTreeElement.expand(); 270 } 271 272 /** 273 * @param {?DOMAgent.NodeId} nodeId 274 */ 275 function selectLastSelectedNode(nodeId) 276 { 277 if (this.selectedDOMNode()) { 278 // Focused node has been explicitly set while reaching out for the last selected node. 279 return; 280 } 281 var node = nodeId ? WebInspector.domAgent.nodeForId(nodeId) : null; 282 selectNode.call(this, node); 283 } 284 285 if (this._selectedPathOnReset) 286 WebInspector.domAgent.pushNodeByPathToFrontend(this._selectedPathOnReset, selectLastSelectedNode.bind(this)); 287 else 288 selectNode.call(this); 289 delete this._selectedPathOnReset; 290 }, 291 292 searchCanceled: function() 293 { 294 delete this._searchQuery; 295 this._hideSearchHighlights(); 296 297 WebInspector.searchController.updateSearchMatchesCount(0, this); 298 299 delete this._currentSearchResultIndex; 300 delete this._searchResults; 301 WebInspector.domAgent.cancelSearch(); 302 }, 303 304 /** 305 * @param {string} query 306 * @param {boolean} shouldJump 307 */ 308 performSearch: function(query, shouldJump) 309 { 310 // Call searchCanceled since it will reset everything we need before doing a new search. 311 this.searchCanceled(); 312 313 const whitespaceTrimmedQuery = query.trim(); 314 if (!whitespaceTrimmedQuery.length) 315 return; 316 317 this._searchQuery = query; 318 319 /** 320 * @param {number} resultCount 321 */ 322 function resultCountCallback(resultCount) 323 { 324 WebInspector.searchController.updateSearchMatchesCount(resultCount, this); 325 if (!resultCount) 326 return; 327 328 this._searchResults = new Array(resultCount); 329 this._currentSearchResultIndex = -1; 330 if (shouldJump) 331 this.jumpToNextSearchResult(); 332 } 333 WebInspector.domAgent.performSearch(whitespaceTrimmedQuery, resultCountCallback.bind(this)); 334 }, 335 336 _contextMenuEventFired: function(event) 337 { 338 function toggleWordWrap() 339 { 340 WebInspector.settings.domWordWrap.set(!WebInspector.settings.domWordWrap.get()); 341 } 342 343 var contextMenu = new WebInspector.ContextMenu(event); 344 this.treeOutline.populateContextMenu(contextMenu, event); 345 346 if (WebInspector.experimentsSettings.cssRegions.isEnabled()) { 347 contextMenu.appendSeparator(); 348 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "CSS named flows\u2026" : "CSS Named Flows\u2026"), this._showNamedFlowCollections.bind(this)); 349 } 350 351 contextMenu.appendSeparator(); 352 contextMenu.appendCheckboxItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Word wrap" : "Word Wrap"), toggleWordWrap.bind(this), WebInspector.settings.domWordWrap.get()); 353 354 contextMenu.show(); 355 }, 356 357 _showNamedFlowCollections: function() 358 { 359 if (!WebInspector.cssNamedFlowCollectionsView) 360 WebInspector.cssNamedFlowCollectionsView = new WebInspector.CSSNamedFlowCollectionsView(); 361 WebInspector.cssNamedFlowCollectionsView.showInDrawer(); 362 }, 363 364 _domWordWrapSettingChanged: function(event) 365 { 366 if (event.data) 367 this.contentElement.removeStyleClass("nowrap"); 368 else 369 this.contentElement.addStyleClass("nowrap"); 370 371 var selectedNode = this.selectedDOMNode(); 372 if (!selectedNode) 373 return; 374 375 var treeElement = this.treeOutline.findTreeElement(selectedNode); 376 if (treeElement) 377 treeElement.updateSelection(); // Recalculate selection highlight dimensions. 378 }, 379 380 switchToAndFocus: function(node) 381 { 382 // Reset search restore. 383 WebInspector.searchController.cancelSearch(); 384 WebInspector.inspectorView.setCurrentPanel(this); 385 this.selectDOMNode(node, true); 386 }, 387 388 _populateContextMenu: function(contextMenu, node) 389 { 390 // Add debbuging-related actions 391 contextMenu.appendSeparator(); 392 var pane = WebInspector.domBreakpointsSidebarPane; 393 pane.populateNodeContextMenu(node, contextMenu); 394 }, 395 396 _getPopoverAnchor: function(element) 397 { 398 var anchor = element.enclosingNodeOrSelfWithClass("webkit-html-resource-link"); 399 if (anchor) { 400 if (!anchor.href) 401 return null; 402 403 var resource = WebInspector.resourceTreeModel.resourceForURL(anchor.href); 404 if (!resource || resource.type !== WebInspector.resourceTypes.Image) 405 return null; 406 407 anchor.removeAttribute("title"); 408 } 409 return anchor; 410 }, 411 412 _loadDimensionsForNode: function(treeElement, callback) 413 { 414 // We get here for CSS properties, too, so bail out early for non-DOM treeElements. 415 if (treeElement.treeOutline !== this.treeOutline) { 416 callback(); 417 return; 418 } 419 420 var node = /** @type {WebInspector.DOMNode} */ (treeElement.representedObject); 421 422 if (!node.nodeName() || node.nodeName().toLowerCase() !== "img") { 423 callback(); 424 return; 425 } 426 427 WebInspector.RemoteObject.resolveNode(node, "", resolvedNode); 428 429 function resolvedNode(object) 430 { 431 if (!object) { 432 callback(); 433 return; 434 } 435 436 object.callFunctionJSON(dimensions, undefined, callback); 437 object.release(); 438 439 function dimensions() 440 { 441 return { offsetWidth: this.offsetWidth, offsetHeight: this.offsetHeight, naturalWidth: this.naturalWidth, naturalHeight: this.naturalHeight }; 442 } 443 } 444 }, 445 446 /** 447 * @param {Element} anchor 448 * @param {WebInspector.Popover} popover 449 */ 450 _showPopover: function(anchor, popover) 451 { 452 var listItem = anchor.enclosingNodeOrSelfWithNodeName("li"); 453 if (listItem && listItem.treeElement) 454 this._loadDimensionsForNode(listItem.treeElement, WebInspector.DOMPresentationUtils.buildImagePreviewContents.bind(WebInspector.DOMPresentationUtils, anchor.href, true, showPopover)); 455 else 456 WebInspector.DOMPresentationUtils.buildImagePreviewContents(anchor.href, true, showPopover); 457 458 /** 459 * @param {Element=} contents 460 */ 461 function showPopover(contents) 462 { 463 if (!contents) 464 return; 465 popover.setCanShrink(false); 466 popover.show(contents, anchor); 467 } 468 }, 469 470 jumpToNextSearchResult: function() 471 { 472 if (!this._searchResults) 473 return; 474 475 this._hideSearchHighlights(); 476 if (++this._currentSearchResultIndex >= this._searchResults.length) 477 this._currentSearchResultIndex = 0; 478 479 this._highlightCurrentSearchResult(); 480 }, 481 482 jumpToPreviousSearchResult: function() 483 { 484 if (!this._searchResults) 485 return; 486 487 this._hideSearchHighlights(); 488 if (--this._currentSearchResultIndex < 0) 489 this._currentSearchResultIndex = (this._searchResults.length - 1); 490 491 this._highlightCurrentSearchResult(); 492 }, 493 494 _highlightCurrentSearchResult: function() 495 { 496 var index = this._currentSearchResultIndex; 497 var searchResults = this._searchResults; 498 var searchResult = searchResults[index]; 499 500 if (searchResult === null) { 501 WebInspector.searchController.updateCurrentMatchIndex(index, this); 502 return; 503 } 504 505 if (typeof searchResult === "undefined") { 506 // No data for slot, request it. 507 function callback(node) 508 { 509 searchResults[index] = node || null; 510 this._highlightCurrentSearchResult(); 511 } 512 WebInspector.domAgent.searchResult(index, callback.bind(this)); 513 return; 514 } 515 516 WebInspector.searchController.updateCurrentMatchIndex(index, this); 517 518 var treeElement = this.treeOutline.findTreeElement(searchResult); 519 if (treeElement) { 520 treeElement.highlightSearchResults(this._searchQuery); 521 treeElement.reveal(); 522 var matches = treeElement.listItemElement.getElementsByClassName("highlighted-search-result"); 523 if (matches.length) 524 matches[0].scrollIntoViewIfNeeded(); 525 } 526 }, 527 528 _hideSearchHighlights: function() 529 { 530 if (!this._searchResults) 531 return; 532 var searchResult = this._searchResults[this._currentSearchResultIndex]; 533 if (!searchResult) 534 return; 535 var treeElement = this.treeOutline.findTreeElement(searchResult); 536 if (treeElement) 537 treeElement.hideSearchHighlights(); 538 }, 539 540 selectedDOMNode: function() 541 { 542 return this.treeOutline.selectedDOMNode(); 543 }, 544 545 /** 546 * @param {boolean=} focus 547 */ 548 selectDOMNode: function(node, focus) 549 { 550 this.treeOutline.selectDOMNode(node, focus); 551 }, 552 553 _nodeRemoved: function(event) 554 { 555 if (!this.isShowing()) 556 return; 557 558 var crumbs = this.crumbsElement; 559 for (var crumb = crumbs.firstChild; crumb; crumb = crumb.nextSibling) { 560 if (crumb.representedObject === event.data.node) { 561 this.updateBreadcrumb(true); 562 return; 563 } 564 } 565 }, 566 567 _stylesPaneEdited: function() 568 { 569 // Once styles are edited, the Metrics pane should be updated. 570 this.sidebarPanes.metrics.needsUpdate = true; 571 this.updateMetrics(); 572 }, 573 574 _metricsPaneEdited: function() 575 { 576 // Once metrics are edited, the Styles pane should be updated. 577 this.sidebarPanes.styles.needsUpdate = true; 578 this.updateStyles(true); 579 }, 580 581 _mouseMovedInCrumbs: function(event) 582 { 583 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); 584 var crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass("crumb"); 585 586 WebInspector.domAgent.highlightDOMNode(crumbElement ? crumbElement.representedObject.id : 0); 587 588 if ("_mouseOutOfCrumbsTimeout" in this) { 589 clearTimeout(this._mouseOutOfCrumbsTimeout); 590 delete this._mouseOutOfCrumbsTimeout; 591 } 592 }, 593 594 _mouseMovedOutOfCrumbs: function(event) 595 { 596 var nodeUnderMouse = document.elementFromPoint(event.pageX, event.pageY); 597 if (nodeUnderMouse && nodeUnderMouse.isDescendant(this.crumbsElement)) 598 return; 599 600 WebInspector.domAgent.hideDOMNodeHighlight(); 601 602 this._mouseOutOfCrumbsTimeout = setTimeout(this.updateBreadcrumbSizes.bind(this), 1000); 603 }, 604 605 _updateBreadcrumbIfNeeded: function(event) 606 { 607 var name = event.data.name; 608 if (name !== "class" && name !== "id") 609 return; 610 611 var node = /** @type {WebInspector.DOMNode} */ (event.data.node); 612 var crumbs = this.crumbsElement; 613 var crumb = crumbs.firstChild; 614 while (crumb) { 615 if (crumb.representedObject === node) { 616 this.updateBreadcrumb(true); 617 break; 618 } 619 crumb = crumb.nextSibling; 620 } 621 }, 622 623 /** 624 * @param {boolean=} forceUpdate 625 */ 626 updateBreadcrumb: function(forceUpdate) 627 { 628 if (!this.isShowing()) 629 return; 630 631 var crumbs = this.crumbsElement; 632 633 var handled = false; 634 var crumb = crumbs.firstChild; 635 while (crumb) { 636 if (crumb.representedObject === this.selectedDOMNode()) { 637 crumb.addStyleClass("selected"); 638 handled = true; 639 } else { 640 crumb.removeStyleClass("selected"); 641 } 642 643 crumb = crumb.nextSibling; 644 } 645 646 if (handled && !forceUpdate) { 647 // We don't need to rebuild the crumbs, but we need to adjust sizes 648 // to reflect the new focused or root node. 649 this.updateBreadcrumbSizes(); 650 return; 651 } 652 653 crumbs.removeChildren(); 654 655 var panel = this; 656 657 function selectCrumbFunction(event) 658 { 659 var crumb = event.currentTarget; 660 if (crumb.hasStyleClass("collapsed")) { 661 // Clicking a collapsed crumb will expose the hidden crumbs. 662 if (crumb === panel.crumbsElement.firstChild) { 663 // If the focused crumb is the first child, pick the farthest crumb 664 // that is still hidden. This allows the user to expose every crumb. 665 var currentCrumb = crumb; 666 while (currentCrumb) { 667 var hidden = currentCrumb.hasStyleClass("hidden"); 668 var collapsed = currentCrumb.hasStyleClass("collapsed"); 669 if (!hidden && !collapsed) 670 break; 671 crumb = currentCrumb; 672 currentCrumb = currentCrumb.nextSibling; 673 } 674 } 675 676 panel.updateBreadcrumbSizes(crumb); 677 } else 678 panel.selectDOMNode(crumb.representedObject, true); 679 680 event.preventDefault(); 681 } 682 683 for (var current = this.selectedDOMNode(); current; current = current.parentNode) { 684 if (current.nodeType() === Node.DOCUMENT_NODE) 685 continue; 686 687 crumb = document.createElement("span"); 688 crumb.className = "crumb"; 689 crumb.representedObject = current; 690 crumb.addEventListener("mousedown", selectCrumbFunction, false); 691 692 var crumbTitle; 693 switch (current.nodeType()) { 694 case Node.ELEMENT_NODE: 695 WebInspector.DOMPresentationUtils.decorateNodeLabel(current, crumb); 696 break; 697 698 case Node.TEXT_NODE: 699 crumbTitle = WebInspector.UIString("(text)"); 700 break 701 702 case Node.COMMENT_NODE: 703 crumbTitle = "<!-->"; 704 break; 705 706 case Node.DOCUMENT_TYPE_NODE: 707 crumbTitle = "<!DOCTYPE>"; 708 break; 709 710 default: 711 crumbTitle = current.nodeNameInCorrectCase(); 712 } 713 714 if (!crumb.childNodes.length) { 715 var nameElement = document.createElement("span"); 716 nameElement.textContent = crumbTitle; 717 crumb.appendChild(nameElement); 718 crumb.title = crumbTitle; 719 } 720 721 if (current === this.selectedDOMNode()) 722 crumb.addStyleClass("selected"); 723 if (!crumbs.childNodes.length) 724 crumb.addStyleClass("end"); 725 726 crumbs.appendChild(crumb); 727 } 728 729 if (crumbs.hasChildNodes()) 730 crumbs.lastChild.addStyleClass("start"); 731 732 this.updateBreadcrumbSizes(); 733 }, 734 735 /** 736 * @param {Element=} focusedCrumb 737 */ 738 updateBreadcrumbSizes: function(focusedCrumb) 739 { 740 if (!this.isShowing()) 741 return; 742 743 if (document.body.offsetWidth <= 0) { 744 // The stylesheet hasn't loaded yet or the window is closed, 745 // so we can't calculate what is need. Return early. 746 return; 747 } 748 749 var crumbs = this.crumbsElement; 750 if (!crumbs.childNodes.length || crumbs.offsetWidth <= 0) 751 return; // No crumbs, do nothing. 752 753 // A Zero index is the right most child crumb in the breadcrumb. 754 var selectedIndex = 0; 755 var focusedIndex = 0; 756 var selectedCrumb; 757 758 var i = 0; 759 var crumb = crumbs.firstChild; 760 while (crumb) { 761 // Find the selected crumb and index. 762 if (!selectedCrumb && crumb.hasStyleClass("selected")) { 763 selectedCrumb = crumb; 764 selectedIndex = i; 765 } 766 767 // Find the focused crumb index. 768 if (crumb === focusedCrumb) 769 focusedIndex = i; 770 771 // Remove any styles that affect size before 772 // deciding to shorten any crumbs. 773 if (crumb !== crumbs.lastChild) 774 crumb.removeStyleClass("start"); 775 if (crumb !== crumbs.firstChild) 776 crumb.removeStyleClass("end"); 777 778 crumb.removeStyleClass("compact"); 779 crumb.removeStyleClass("collapsed"); 780 crumb.removeStyleClass("hidden"); 781 782 crumb = crumb.nextSibling; 783 ++i; 784 } 785 786 // Restore the start and end crumb classes in case they got removed in coalesceCollapsedCrumbs(). 787 // The order of the crumbs in the document is opposite of the visual order. 788 crumbs.firstChild.addStyleClass("end"); 789 crumbs.lastChild.addStyleClass("start"); 790 791 var rightPadding = 20; 792 var crumbsTotalOffsetLeft = crumbs.totalOffsetLeft(); 793 var windowInnerWidth = window.innerWidth; 794 var errorWarningElement = document.getElementById("error-warning-count"); 795 if (!WebInspector.drawer.visible) { 796 if (errorWarningElement) 797 rightPadding += errorWarningElement.offsetWidth; 798 rightPadding += WebInspector.settingsController.statusBarItem.offsetWidth; 799 } 800 801 function crumbsAreSmallerThanContainer() 802 { 803 return (crumbsTotalOffsetLeft + crumbs.offsetWidth + rightPadding) < windowInnerWidth; 804 } 805 806 if (crumbsAreSmallerThanContainer()) 807 return; // No need to compact the crumbs, they all fit at full size. 808 809 var BothSides = 0; 810 var AncestorSide = -1; 811 var ChildSide = 1; 812 813 /** 814 * @param {boolean=} significantCrumb 815 */ 816 function makeCrumbsSmaller(shrinkingFunction, direction, significantCrumb) 817 { 818 if (!significantCrumb) 819 significantCrumb = (focusedCrumb || selectedCrumb); 820 821 if (significantCrumb === selectedCrumb) 822 var significantIndex = selectedIndex; 823 else if (significantCrumb === focusedCrumb) 824 var significantIndex = focusedIndex; 825 else { 826 var significantIndex = 0; 827 for (var i = 0; i < crumbs.childNodes.length; ++i) { 828 if (crumbs.childNodes[i] === significantCrumb) { 829 significantIndex = i; 830 break; 831 } 832 } 833 } 834 835 function shrinkCrumbAtIndex(index) 836 { 837 var shrinkCrumb = crumbs.childNodes[index]; 838 if (shrinkCrumb && shrinkCrumb !== significantCrumb) 839 shrinkingFunction(shrinkCrumb); 840 if (crumbsAreSmallerThanContainer()) 841 return true; // No need to compact the crumbs more. 842 return false; 843 } 844 845 // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs 846 // fit in the container or we run out of crumbs to shrink. 847 if (direction) { 848 // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb. 849 var index = (direction > 0 ? 0 : crumbs.childNodes.length - 1); 850 while (index !== significantIndex) { 851 if (shrinkCrumbAtIndex(index)) 852 return true; 853 index += (direction > 0 ? 1 : -1); 854 } 855 } else { 856 // Crumbs are shrunk in order of descending distance from the signifcant crumb, 857 // with a tie going to child crumbs. 858 var startIndex = 0; 859 var endIndex = crumbs.childNodes.length - 1; 860 while (startIndex != significantIndex || endIndex != significantIndex) { 861 var startDistance = significantIndex - startIndex; 862 var endDistance = endIndex - significantIndex; 863 if (startDistance >= endDistance) 864 var index = startIndex++; 865 else 866 var index = endIndex--; 867 if (shrinkCrumbAtIndex(index)) 868 return true; 869 } 870 } 871 872 // We are not small enough yet, return false so the caller knows. 873 return false; 874 } 875 876 function coalesceCollapsedCrumbs() 877 { 878 var crumb = crumbs.firstChild; 879 var collapsedRun = false; 880 var newStartNeeded = false; 881 var newEndNeeded = false; 882 while (crumb) { 883 var hidden = crumb.hasStyleClass("hidden"); 884 if (!hidden) { 885 var collapsed = crumb.hasStyleClass("collapsed"); 886 if (collapsedRun && collapsed) { 887 crumb.addStyleClass("hidden"); 888 crumb.removeStyleClass("compact"); 889 crumb.removeStyleClass("collapsed"); 890 891 if (crumb.hasStyleClass("start")) { 892 crumb.removeStyleClass("start"); 893 newStartNeeded = true; 894 } 895 896 if (crumb.hasStyleClass("end")) { 897 crumb.removeStyleClass("end"); 898 newEndNeeded = true; 899 } 900 901 continue; 902 } 903 904 collapsedRun = collapsed; 905 906 if (newEndNeeded) { 907 newEndNeeded = false; 908 crumb.addStyleClass("end"); 909 } 910 } else 911 collapsedRun = true; 912 crumb = crumb.nextSibling; 913 } 914 915 if (newStartNeeded) { 916 crumb = crumbs.lastChild; 917 while (crumb) { 918 if (!crumb.hasStyleClass("hidden")) { 919 crumb.addStyleClass("start"); 920 break; 921 } 922 crumb = crumb.previousSibling; 923 } 924 } 925 } 926 927 function compact(crumb) 928 { 929 if (crumb.hasStyleClass("hidden")) 930 return; 931 crumb.addStyleClass("compact"); 932 } 933 934 function collapse(crumb, dontCoalesce) 935 { 936 if (crumb.hasStyleClass("hidden")) 937 return; 938 crumb.addStyleClass("collapsed"); 939 crumb.removeStyleClass("compact"); 940 if (!dontCoalesce) 941 coalesceCollapsedCrumbs(); 942 } 943 944 if (!focusedCrumb) { 945 // When not focused on a crumb we can be biased and collapse less important 946 // crumbs that the user might not care much about. 947 948 // Compact child crumbs. 949 if (makeCrumbsSmaller(compact, ChildSide)) 950 return; 951 952 // Collapse child crumbs. 953 if (makeCrumbsSmaller(collapse, ChildSide)) 954 return; 955 } 956 957 // Compact ancestor crumbs, or from both sides if focused. 958 if (makeCrumbsSmaller(compact, (focusedCrumb ? BothSides : AncestorSide))) 959 return; 960 961 // Collapse ancestor crumbs, or from both sides if focused. 962 if (makeCrumbsSmaller(collapse, (focusedCrumb ? BothSides : AncestorSide))) 963 return; 964 965 if (!selectedCrumb) 966 return; 967 968 // Compact the selected crumb. 969 compact(selectedCrumb); 970 if (crumbsAreSmallerThanContainer()) 971 return; 972 973 // Collapse the selected crumb as a last resort. Pass true to prevent coalescing. 974 collapse(selectedCrumb, true); 975 }, 976 977 /** 978 * @param {boolean=} forceUpdate 979 */ 980 updateStyles: function(forceUpdate) 981 { 982 var stylesSidebarPane = this.sidebarPanes.styles; 983 var computedStylePane = this.sidebarPanes.computedStyle; 984 if ((!stylesSidebarPane.isShowing() && !computedStylePane.isShowing()) || !stylesSidebarPane.needsUpdate) 985 return; 986 987 stylesSidebarPane.update(this.selectedDOMNode(), forceUpdate); 988 stylesSidebarPane.needsUpdate = false; 989 }, 990 991 updateMetrics: function() 992 { 993 var metricsSidebarPane = this.sidebarPanes.metrics; 994 if (!metricsSidebarPane.isShowing() || !metricsSidebarPane.needsUpdate) 995 return; 996 997 metricsSidebarPane.update(this.selectedDOMNode()); 998 metricsSidebarPane.needsUpdate = false; 999 }, 1000 1001 updateProperties: function() 1002 { 1003 var propertiesSidebarPane = this.sidebarPanes.properties; 1004 if (!propertiesSidebarPane.isShowing() || !propertiesSidebarPane.needsUpdate) 1005 return; 1006 1007 propertiesSidebarPane.update(this.selectedDOMNode()); 1008 propertiesSidebarPane.needsUpdate = false; 1009 }, 1010 1011 updateEventListeners: function() 1012 { 1013 var eventListenersSidebarPane = this.sidebarPanes.eventListeners; 1014 if (!eventListenersSidebarPane.isShowing() || !eventListenersSidebarPane.needsUpdate) 1015 return; 1016 1017 eventListenersSidebarPane.update(this.selectedDOMNode()); 1018 eventListenersSidebarPane.needsUpdate = false; 1019 }, 1020 1021 handleShortcut: function(event) 1022 { 1023 function handleUndoRedo() 1024 { 1025 if (WebInspector.KeyboardShortcut.eventHasCtrlOrMeta(event) && !event.shiftKey && event.keyIdentifier === "U+005A") { // Z key 1026 WebInspector.domAgent.undo(this._updateSidebars.bind(this)); 1027 event.handled = true; 1028 return; 1029 } 1030 1031 var isRedoKey = WebInspector.isMac() ? event.metaKey && event.shiftKey && event.keyIdentifier === "U+005A" : // Z key 1032 event.ctrlKey && event.keyIdentifier === "U+0059"; // Y key 1033 if (isRedoKey) { 1034 DOMAgent.redo(this._updateSidebars.bind(this)); 1035 event.handled = true; 1036 } 1037 } 1038 1039 if (!this.treeOutline.editing()) { 1040 handleUndoRedo.call(this); 1041 if (event.handled) 1042 return; 1043 } 1044 1045 this.treeOutline.handleShortcut(event); 1046 }, 1047 1048 handleCopyEvent: function(event) 1049 { 1050 var currentFocusElement = WebInspector.currentFocusElement(); 1051 if (currentFocusElement && WebInspector.isBeingEdited(currentFocusElement)) 1052 return; 1053 1054 // Don't prevent the normal copy if the user has a selection. 1055 if (!window.getSelection().isCollapsed) 1056 return; 1057 event.clipboardData.clearData(); 1058 event.preventDefault(); 1059 this.selectedDOMNode().copyNode(); 1060 }, 1061 1062 sidebarResized: function(event) 1063 { 1064 this.treeOutline.updateSelection(); 1065 }, 1066 1067 revealAndSelectNode: function(nodeId) 1068 { 1069 WebInspector.inspectorView.setCurrentPanel(this); 1070 1071 var node = WebInspector.domAgent.nodeForId(nodeId); 1072 if (!node) 1073 return; 1074 1075 while (!WebInspector.ElementsTreeOutline.showShadowDOM() && node && node.isInShadowTree()) 1076 node = node.parentNode; 1077 1078 WebInspector.domAgent.highlightDOMNodeForTwoSeconds(nodeId); 1079 this.selectDOMNode(node, true); 1080 }, 1081 1082 /** 1083 * @param {WebInspector.ContextMenu} contextMenu 1084 * @param {Object} target 1085 */ 1086 appendApplicableItems: function(event, contextMenu, target) 1087 { 1088 if (!(target instanceof WebInspector.RemoteObject)) 1089 return; 1090 var remoteObject = /** @type {WebInspector.RemoteObject} */ (target); 1091 if (remoteObject.subtype !== "node") 1092 return; 1093 1094 function selectNode(nodeId) 1095 { 1096 if (nodeId) 1097 WebInspector.domAgent.inspectElement(nodeId); 1098 } 1099 1100 function revealElement() 1101 { 1102 remoteObject.pushNodeToFrontend(selectNode); 1103 } 1104 1105 contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Reveal in Elements panel" : "Reveal in Elements Panel"), revealElement.bind(this)); 1106 }, 1107 1108 _sidebarContextMenuEventFired: function(event) 1109 { 1110 var contextMenu = new WebInspector.ContextMenu(event); 1111 contextMenu.show(); 1112 }, 1113 1114 _dockSideChanged: function() 1115 { 1116 var dockSide = WebInspector.dockController.dockSide(); 1117 var vertically = dockSide === WebInspector.DockController.State.DockedToRight && WebInspector.settings.splitVerticallyWhenDockedToRight.get(); 1118 this._splitVertically(vertically); 1119 }, 1120 1121 _showShadowDOMChanged: function() 1122 { 1123 this.treeOutline.update(); 1124 }, 1125 1126 /** 1127 * @param {boolean} vertically 1128 */ 1129 _splitVertically: function(vertically) 1130 { 1131 if (this.sidebarPaneView && vertically === !this.splitView.isVertical()) 1132 return; 1133 1134 if (this.sidebarPaneView) 1135 this.sidebarPaneView.detach(); 1136 1137 this.splitView.setVertical(!vertically); 1138 1139 var computedPane = new WebInspector.SidebarPane(WebInspector.UIString("Computed")); 1140 computedPane.element.addStyleClass("composite"); 1141 computedPane.element.addStyleClass("fill"); 1142 var expandComputed = computedPane.expand.bind(computedPane); 1143 1144 computedPane.bodyElement.appendChild(this.sidebarPanes.computedStyle.titleElement); 1145 computedPane.bodyElement.addStyleClass("metrics-and-computed"); 1146 this.sidebarPanes.computedStyle.show(computedPane.bodyElement); 1147 this.sidebarPanes.computedStyle.setExpandCallback(expandComputed); 1148 1149 if (vertically) { 1150 this.sidebarPanes.metrics.show(computedPane.bodyElement, this.sidebarPanes.computedStyle.element); 1151 this.sidebarPanes.metrics.setExpandCallback(expandComputed); 1152 1153 this.sidebarPaneView = new WebInspector.SidebarTabbedPane(); 1154 1155 var compositePane = new WebInspector.SidebarPane(this.sidebarPanes.styles.title()); 1156 compositePane.element.addStyleClass("composite"); 1157 compositePane.element.addStyleClass("fill"); 1158 var expandComposite = compositePane.expand.bind(compositePane); 1159 1160 var splitView = new WebInspector.SplitView(true, "StylesPaneSplitRatio", 0.5); 1161 splitView.show(compositePane.bodyElement); 1162 1163 this.sidebarPanes.styles.show(splitView.firstElement()); 1164 splitView.firstElement().appendChild(this.sidebarPanes.styles.titleElement); 1165 this.sidebarPanes.styles.setExpandCallback(expandComposite); 1166 1167 computedPane.show(splitView.secondElement()); 1168 computedPane.setExpandCallback(expandComposite); 1169 1170 this.sidebarPaneView.addPane(compositePane); 1171 this.sidebarPaneView.addPane(this.sidebarPanes.properties); 1172 this.sidebarPaneView.addPane(this.sidebarPanes.domBreakpoints); 1173 this.sidebarPaneView.addPane(this.sidebarPanes.eventListeners); 1174 } else { 1175 this.sidebarPaneView = new WebInspector.SidebarTabbedPane(); 1176 1177 var stylesPane = new WebInspector.SidebarPane(this.sidebarPanes.styles.title()); 1178 stylesPane.element.addStyleClass("composite"); 1179 stylesPane.element.addStyleClass("fill"); 1180 var expandStyles = stylesPane.expand.bind(stylesPane); 1181 stylesPane.bodyElement.addStyleClass("metrics-and-styles"); 1182 this.sidebarPanes.styles.show(stylesPane.bodyElement); 1183 this.sidebarPanes.styles.setExpandCallback(expandStyles); 1184 this.sidebarPanes.metrics.setExpandCallback(expandStyles); 1185 stylesPane.bodyElement.appendChild(this.sidebarPanes.styles.titleElement); 1186 1187 /** 1188 * @param {WebInspector.SidebarPane} pane 1189 * @param {Element=} beforeElement 1190 */ 1191 function showMetrics(pane, beforeElement) 1192 { 1193 this.sidebarPanes.metrics.show(pane.bodyElement, beforeElement); 1194 } 1195 1196 /** 1197 * @param {WebInspector.Event} event 1198 */ 1199 function tabSelected(event) 1200 { 1201 var tabId = /** @type {string} */ (event.data.tabId); 1202 if (tabId === computedPane.title()) 1203 showMetrics.call(this, computedPane, this.sidebarPanes.computedStyle.element); 1204 if (tabId === stylesPane.title()) 1205 showMetrics.call(this, stylesPane); 1206 } 1207 1208 this.sidebarPaneView.addEventListener(WebInspector.TabbedPane.EventTypes.TabSelected, tabSelected, this); 1209 1210 showMetrics.call(this, stylesPane); 1211 this.sidebarPaneView.addPane(stylesPane); 1212 this.sidebarPaneView.addPane(computedPane); 1213 1214 this.sidebarPaneView.addPane(this.sidebarPanes.eventListeners); 1215 this.sidebarPaneView.addPane(this.sidebarPanes.domBreakpoints); 1216 this.sidebarPaneView.addPane(this.sidebarPanes.properties); 1217 } 1218 1219 this.sidebarPaneView.show(this.splitView.sidebarElement); 1220 this.sidebarPanes.styles.expand(); 1221 }, 1222 1223 /** 1224 * @param {string} id 1225 * @param {WebInspector.SidebarPane} pane 1226 */ 1227 addExtensionSidebarPane: function(id, pane) 1228 { 1229 this.sidebarPanes[id] = pane; 1230 this.sidebarPaneView.addPane(pane); 1231 }, 1232 1233 __proto__: WebInspector.Panel.prototype 1234 } 1235