UNPKG

chrome-devtools-frontend

Version:
1,489 lines (1,320 loc) • 49.9 kB
/* * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. * Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com> * Copyright (C) 2009 Joseph Pecoraro * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Common from '../common/common.js'; import * as Components from '../components/components.js'; import * as Extensions from '../extensions/extensions.js'; import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as Root from '../root/root.js'; import * as SDK from '../sdk/sdk.js'; import * as UI from '../ui/ui.js'; import {AccessibilityTreeView} from './AccessibilityTreeView.js'; import {ComputedStyleWidget} from './ComputedStyleWidget.js'; import {DOMNode, ElementsBreadcrumbs} from './ElementsBreadcrumbs.js'; // eslint-disable-line no-unused-vars import {ElementsTreeElement} from './ElementsTreeElement.js'; // eslint-disable-line no-unused-vars import {ElementsTreeElementHighlighter} from './ElementsTreeElementHighlighter.js'; import {ElementsTreeOutline} from './ElementsTreeOutline.js'; import {MarkerDecorator} from './MarkerDecorator.js'; // eslint-disable-line no-unused-vars import {MetricsSidebarPane} from './MetricsSidebarPane.js'; import {Events as StylesSidebarPaneEvents, StylesSidebarPane} from './StylesSidebarPane.js'; export const UIStrings = { /** *@description Text in Elements Panel of the Elements panel */ findByStringSelectorOrXpath: 'Find by string, selector, or XPath', /** *@description Title of the switch to accessibility tree button in the Elements panel */ switchToAccessibilityTreeView: 'Switch to Accessibility Tree view', /** *@description Title of the switch to DOM tree button in the Elements panel */ switchToDomTreeView: 'Switch to DOM Tree view', /** *@description Text for a rendering frame */ frame: 'Frame', /** *@description Title of the Computed Styles sidebar toggle in the Styles pane */ computedStylesSidebar: 'Computed Styles sidebar', /** *@description Text in Elements Panel of the Elements panel */ computed: 'Computed', /** *@description Text in Elements Panel of the Elements panel */ styles: 'Styles', /** *@description A context menu item to reveal a node in the DOM tree of the Elements Panel */ revealInElementsPanel: 'Reveal in Elements panel', /** *@description Text when node can not be found in page */ nodeCannotBeFoundInTheCurrent: 'Node cannot be found in the current page.', /** *@description Console warning when a user tries to reveal a non-node type Remote Object. */ theRemoteObjectCouldNotBe: 'The remote object could not be resolved into a valid node.', /** *@description Console warning when the user tries to reveal a deferred DOM Node that resolves as null. */ theDeferredDomNodeCouldNotBe: 'The deferred DOM Node could not be resolved into a valid node.', /** *@description Text in Elements Panel of the Elements panel *@example {::after, ::before} PH1 */ elementStateS: 'Element state: {PH1}', }; const str_ = i18n.i18n.registerUIStrings('elements/ElementsPanel.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** * * @param {!SDK.DOMModel.DOMNode} node * @return {!DOMNode} */ const legacyNodeToNewBreadcrumbsNode = node => { return { parentNode: node.parentNode ? legacyNodeToNewBreadcrumbsNode(node.parentNode) : null, id: /** @type {number} */ (node.id), nodeType: node.nodeType(), pseudoType: node.pseudoType(), shadowRootType: node.shadowRootType(), nodeName: node.nodeName(), nodeNameNicelyCased: node.nodeNameInCorrectCase(), legacyDomNode: node, highlightNode: () => node.highlight(), clearHighlight: () => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(), getAttribute: node.getAttribute.bind(node), }; }; /** @type {!ElementsPanel} */ let elementsPanelInstance; /** * @implements {UI.SearchableView.Searchable} * @implements {SDK.SDKModel.SDKModelObserver<!SDK.DOMModel.DOMModel>} * @implements {UI.View.ViewLocationResolver} */ export class ElementsPanel extends UI.Panel.Panel { constructor() { super('elements'); this.registerRequiredCSS('elements/elementsPanel.css', {enableLegacyPatching: true}); this._splitWidget = new UI.SplitWidget.SplitWidget(true, true, 'elementsPanelSplitViewState', 325, 325); this._splitWidget.addEventListener( UI.SplitWidget.Events.SidebarSizeChanged, this._updateTreeOutlineVisibleWidth.bind(this)); this._splitWidget.show(this.element); this._searchableView = new UI.SearchableView.SearchableView(this, null); this._searchableView.setMinimumSize(25, 28); this._searchableView.setPlaceholder(i18nString(UIStrings.findByStringSelectorOrXpath)); const stackElement = this._searchableView.element; this._contentElement = document.createElement('div'); const crumbsContainer = document.createElement('div'); if (Root.Runtime.experiments.isEnabled('fullAccessibilityTree')) { this._initializeFullAccessibilityTreeView(stackElement); } stackElement.appendChild(this._contentElement); stackElement.appendChild(crumbsContainer); this._splitWidget.setMainWidget(this._searchableView); /** @type {?_splitMode} */ this._splitMode = null; this._contentElement.id = 'elements-content'; // FIXME: crbug.com/425984 if (Common.Settings.Settings.instance().moduleSetting('domWordWrap').get()) { this._contentElement.classList.add('elements-wrap'); } Common.Settings.Settings.instance() .moduleSetting('domWordWrap') .addChangeListener(this._domWordWrapSettingChanged.bind(this)); crumbsContainer.id = 'elements-crumbs'; if (this.domTreeButton) { this._accessibilityTreeView = new AccessibilityTreeView(this.domTreeButton); } this._breadcrumbs = new ElementsBreadcrumbs(); this._breadcrumbs.addEventListener('node-selected', /** @param {!Event} event */ event => { this._crumbNodeSelected(/** @type {?} */ (event)); }); crumbsContainer.appendChild(this._breadcrumbs); this._stylesWidget = StylesSidebarPane.instance(); this._computedStyleWidget = new ComputedStyleWidget(); this._metricsWidget = new MetricsSidebarPane(); Common.Settings.Settings.instance() .moduleSetting('sidebarPosition') .addChangeListener(this._updateSidebarPosition.bind(this)); this._updateSidebarPosition(); /** @type {!Set.<!ElementsTreeOutline>} */ this._treeOutlines = new Set(); /** @type {!Map<!ElementsTreeOutline, !Element>} */ this._treeOutlineHeaders = new Map(); /** @type {!Map<!SDK.CSSModel.CSSModel, !SDK.CSSModel.CSSPropertyTracker>} */ this._gridStyleTrackerByCSSModel = new Map(); SDK.SDKModel.TargetManager.instance().observeModels(SDK.DOMModel.DOMModel, this); SDK.SDKModel.TargetManager.instance().addEventListener( SDK.SDKModel.Events.NameChanged, event => this._targetNameChanged(/** @type {!SDK.SDKModel.Target} */ (event.data))); Common.Settings.Settings.instance() .moduleSetting('showUAShadowDOM') .addChangeListener(this._showUAShadowDOMChanged.bind(this)); SDK.SDKModel.TargetManager.instance().addModelListener( SDK.DOMModel.DOMModel, SDK.DOMModel.Events.DocumentUpdated, this._documentUpdatedEvent, this); Extensions.ExtensionServer.ExtensionServer.instance().addEventListener( Extensions.ExtensionServer.Events.SidebarPaneAdded, this._extensionSidebarPaneAdded, this); /** * @type {!Array.<{domModel: !SDK.DOMModel.DOMModel, index: number, node: (?SDK.DOMModel.DOMNode|undefined)}>|undefined} */ this._searchResults; this._currentSearchResultIndex = -1; // -1 represents the initial invalid state this._pendingNodeReveal = false; } /** * @param {UI.Widget.WidgetElement} stackElement */ _initializeFullAccessibilityTreeView(stackElement) { this._accessibilityTreeButton = document.createElement('button'); this._accessibilityTreeButton.textContent = i18nString(UIStrings.switchToAccessibilityTreeView); this._accessibilityTreeButton.addEventListener('click', this._showAccessibilityTree.bind(this)); this.domTreeButton = document.createElement('button'); this.domTreeButton.textContent = i18nString(UIStrings.switchToDomTreeView); this.domTreeButton.addEventListener('click', this._showDOMTree.bind(this)); stackElement.appendChild(this._accessibilityTreeButton); } _showAccessibilityTree() { if (this._accessibilityTreeView) { this._splitWidget.setMainWidget(this._accessibilityTreeView); } } _showDOMTree() { this._splitWidget.setMainWidget(this._searchableView); } /** * @param {{forceNew: ?boolean}=} opts * @return {!ElementsPanel} */ static instance(opts = {forceNew: null}) { const {forceNew} = opts; if (!elementsPanelInstance || forceNew) { elementsPanelInstance = new ElementsPanel(); } return elementsPanelInstance; } /** * @param {!SDK.CSSProperty.CSSProperty} cssProperty */ _revealProperty(cssProperty) { if (!this.sidebarPaneView || !this._stylesViewToReveal) { return Promise.resolve(); } return this.sidebarPaneView.showView(this._stylesViewToReveal).then(() => { this._stylesWidget.revealProperty(/** @type {!SDK.CSSProperty.CSSProperty} */ (cssProperty)); }); } /** * @override * @param {string} locationName * @return {?UI.View.ViewLocation} */ resolveLocation(locationName) { return this.sidebarPaneView || null; } /** * @param {?UI.Widget.Widget} widget * @param {?UI.Toolbar.ToolbarToggle} toggle */ showToolbarPane(widget, toggle) { // TODO(luoe): remove this function once its providers have an alternative way to reveal their views. this._stylesWidget.showToolbarPane(widget, toggle); } /** * @override * @param {!SDK.DOMModel.DOMModel} domModel */ modelAdded(domModel) { const parentModel = domModel.parentModel(); let treeOutline = parentModel ? ElementsTreeOutline.forDOMModel(parentModel) : null; if (!treeOutline) { treeOutline = new ElementsTreeOutline(true, true); treeOutline.setWordWrap(Common.Settings.Settings.instance().moduleSetting('domWordWrap').get()); treeOutline.addEventListener(ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedNodeChanged, this); treeOutline.addEventListener( ElementsTreeOutline.Events.ElementsTreeUpdated, this._updateBreadcrumbIfNeeded, this); new ElementsTreeElementHighlighter(treeOutline); this._treeOutlines.add(treeOutline); if (domModel.target().parentTarget()) { const element = document.createElement('div'); element.classList.add('elements-tree-header'); this._treeOutlineHeaders.set(treeOutline, element); this._targetNameChanged(domModel.target()); } } treeOutline.wireToDOMModel(domModel); this._setupStyleTracking(domModel.cssModel()); // Perform attach if necessary. if (this.isShowing()) { this.wasShown(); } } /** * @override * @param {!SDK.DOMModel.DOMModel} domModel */ modelRemoved(domModel) { const treeOutline = ElementsTreeOutline.forDOMModel(domModel); if (!treeOutline) { return; } treeOutline.unwireFromDOMModel(domModel); if (domModel.parentModel()) { return; } this._treeOutlines.delete(treeOutline); const header = this._treeOutlineHeaders.get(treeOutline); if (header) { header.remove(); } this._treeOutlineHeaders.delete(treeOutline); treeOutline.element.remove(); this._removeStyleTracking(domModel.cssModel()); } /** * @param {!SDK.SDKModel.Target} target */ _targetNameChanged(target) { const domModel = target.model(SDK.DOMModel.DOMModel); if (!domModel) { return; } const treeOutline = ElementsTreeOutline.forDOMModel(domModel); if (!treeOutline) { return; } const header = this._treeOutlineHeaders.get(treeOutline); if (!header) { return; } header.removeChildren(); header.createChild('div', 'elements-tree-header-frame').textContent = i18nString(UIStrings.frame); header.appendChild(Components.Linkifier.Linkifier.linkifyURL( target.inspectedURL(), /** @type {!Components.Linkifier.LinkifyURLOptions} */ ({text: target.name()}))); } _updateTreeOutlineVisibleWidth() { if (!this._treeOutlines.size) { return; } let width = this._splitWidget.element.offsetWidth; if (this._splitWidget.isVertical()) { width -= this._splitWidget.sidebarSize(); } for (const treeOutline of this._treeOutlines) { treeOutline.setVisibleWidth(width); } } /** * @override */ focus() { if (this._treeOutlines.size) { this._treeOutlines.values().next().value.focus(); } } /** * @override * @return {!UI.SearchableView.SearchableView} */ searchableView() { return this._searchableView; } /** * @override */ wasShown() { UI.Context.Context.instance().setFlavor(ElementsPanel, this); for (const treeOutline of this._treeOutlines) { // Attach heavy component lazily if (treeOutline.element.parentElement !== this._contentElement) { const header = this._treeOutlineHeaders.get(treeOutline); if (header) { this._contentElement.appendChild(header); } this._contentElement.appendChild(treeOutline.element); } } super.wasShown(); const domModels = SDK.SDKModel.TargetManager.instance().models(SDK.DOMModel.DOMModel); for (const domModel of domModels) { if (domModel.parentModel()) { continue; } const treeOutline = ElementsTreeOutline.forDOMModel(domModel); if (!treeOutline) { continue; } treeOutline.setVisible(true); if (!treeOutline.rootDOMNode) { if (domModel.existingDocument()) { treeOutline.rootDOMNode = domModel.existingDocument(); this._documentUpdated(domModel); } else { domModel.requestDocument(); } } } } /** * @override */ willHide() { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); for (const treeOutline of this._treeOutlines) { treeOutline.setVisible(false); // Detach heavy component on hide this._contentElement.removeChild(treeOutline.element); const header = this._treeOutlineHeaders.get(treeOutline); if (header) { this._contentElement.removeChild(header); } } super.willHide(); UI.Context.Context.instance().setFlavor(ElementsPanel, null); } /** * @override */ onResize() { this.element.window().requestAnimationFrame(this._updateSidebarPosition.bind(this)); // Do not force layout. this._updateTreeOutlineVisibleWidth(); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _selectedNodeChanged(event) { let selectedNode = /** @type {?SDK.DOMModel.DOMNode} */ (event.data.node); // If the selectedNode is a pseudoNode, we want to ensure that it has a valid parentNode if (selectedNode && (selectedNode.pseudoType() && !selectedNode.parentNode)) { selectedNode = null; } const focus = /** @type {boolean} */ (event.data.focus); for (const treeOutline of this._treeOutlines) { if (!selectedNode || ElementsTreeOutline.forDOMModel(selectedNode.domModel()) !== treeOutline) { treeOutline.selectDOMNode(null); } } if (selectedNode) { const activeNode = legacyNodeToNewBreadcrumbsNode(selectedNode); const crumbs = [activeNode]; for (let current = selectedNode.parentNode; current; current = current.parentNode) { crumbs.push(legacyNodeToNewBreadcrumbsNode(current)); } this._breadcrumbs.data = { crumbs, selectedNode: legacyNodeToNewBreadcrumbsNode(selectedNode), }; } else { this._breadcrumbs.data = {crumbs: [], selectedNode: null}; } UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, selectedNode); if (!selectedNode) { return; } selectedNode.setAsInspectedNode(); if (this._accessibilityTreeView) { this._accessibilityTreeView.setNode(selectedNode); } if (focus) { this._selectedNodeOnReset = selectedNode; this._hasNonDefaultSelectedNode = true; } const executionContexts = selectedNode.domModel().runtimeModel().executionContexts(); const nodeFrameId = selectedNode.frameId(); for (const context of executionContexts) { if (context.frameId === nodeFrameId) { UI.Context.Context.instance().setFlavor(SDK.RuntimeModel.ExecutionContext, context); break; } } } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _documentUpdatedEvent(event) { const domModel = /** @type {!SDK.DOMModel.DOMModel} */ (event.data); this._documentUpdated(domModel); this._removeStyleTracking(domModel.cssModel()); this._setupStyleTracking(domModel.cssModel()); } /** * @param {!SDK.DOMModel.DOMModel} domModel */ _documentUpdated(domModel) { this._searchableView.resetSearch(); if (!domModel.existingDocument()) { if (this.isShowing()) { domModel.requestDocument(); } return; } this._hasNonDefaultSelectedNode = false; if (this._omitDefaultSelection) { return; } const savedSelectedNodeOnReset = this._selectedNodeOnReset; restoreNode.call(this, domModel, this._selectedNodeOnReset || null); /** * @param {!SDK.DOMModel.DOMModel} domModel * @param {?SDK.DOMModel.DOMNode} staleNode * @this {ElementsPanel} */ async function restoreNode(domModel, staleNode) { const nodePath = staleNode ? staleNode.path() : null; const restoredNodeId = nodePath ? await domModel.pushNodeByPathToFrontend(nodePath) : null; if (savedSelectedNodeOnReset !== this._selectedNodeOnReset) { return; } let node = restoredNodeId ? domModel.nodeForId(restoredNodeId) : null; if (!node) { const inspectedDocument = domModel.existingDocument(); node = inspectedDocument ? inspectedDocument.body || inspectedDocument.documentElement : null; } // If `node` is null here, the document hasn't been transmitted from the backend yet // and isn't in a valid state to have a default-selected node. Another document update // should be forthcoming. In the meantime, don't set the default-selected node or notify // the test that it's ready, because it isn't. if (node) { this._setDefaultSelectedNode(node); this._lastSelectedNodeSelectedForTest(); } } } _lastSelectedNodeSelectedForTest() { } /** * @param {?SDK.DOMModel.DOMNode} node */ _setDefaultSelectedNode(node) { if (!node || this._hasNonDefaultSelectedNode || this._pendingNodeReveal) { return; } const treeOutline = ElementsTreeOutline.forDOMModel(node.domModel()); if (!treeOutline) { return; } this.selectDOMNode(node); if (treeOutline.selectedTreeElement) { treeOutline.selectedTreeElement.expand(); } } /** * @override */ searchCanceled() { this._searchConfig = undefined; this._hideSearchHighlights(); this._searchableView.updateSearchMatchesCount(0); this._currentSearchResultIndex = -1; delete this._searchResults; SDK.DOMModel.DOMModel.cancelSearch(); } /** * @override * @param {!UI.SearchableView.SearchConfig} searchConfig * @param {boolean} shouldJump * @param {boolean=} jumpBackwards */ performSearch(searchConfig, shouldJump, jumpBackwards) { const query = searchConfig.query; const whitespaceTrimmedQuery = query.trim(); if (!whitespaceTrimmedQuery.length) { return; } if (!this._searchConfig || this._searchConfig.query !== query) { this.searchCanceled(); } else { this._hideSearchHighlights(); } this._searchConfig = searchConfig; const showUAShadowDOM = Common.Settings.Settings.instance().moduleSetting('showUAShadowDOM').get(); const domModels = SDK.SDKModel.TargetManager.instance().models(SDK.DOMModel.DOMModel); const promises = domModels.map(domModel => domModel.performSearch(whitespaceTrimmedQuery, showUAShadowDOM)); Promise.all(promises).then(resultCounts => { this._searchResults = []; for (let i = 0; i < resultCounts.length; ++i) { const resultCount = resultCounts[i]; for (let j = 0; j < resultCount; ++j) { this._searchResults.push({domModel: domModels[i], index: j, node: undefined}); } } this._searchableView.updateSearchMatchesCount(this._searchResults.length); if (!this._searchResults.length) { return; } if (this._currentSearchResultIndex >= this._searchResults.length) { this._currentSearchResultIndex = -1; } let index = this._currentSearchResultIndex; if (shouldJump) { if (this._currentSearchResultIndex === -1) { index = jumpBackwards ? -1 : 0; } else { index = jumpBackwards ? index - 1 : index + 1; } this._jumpToSearchResult(index); } }); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _domWordWrapSettingChanged(event) { this._contentElement.classList.toggle('elements-wrap', /** @type {boolean} */ (event.data)); for (const treeOutline of this._treeOutlines) { treeOutline.setWordWrap(/** @type {boolean} */ (event.data)); } } /** * @param {!SDK.DOMModel.DOMNode} node */ switchToAndFocus(node) { // Reset search restore. this._searchableView.cancelSearch(); UI.ViewManager.ViewManager.instance().showView('elements').then(() => this.selectDOMNode(node, true)); } /** * @param {number} index */ _jumpToSearchResult(index) { if (!this._searchResults) { return; } this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length; this._highlightCurrentSearchResult(); } /** * @override */ jumpToNextSearchResult() { if (!this._searchResults || !this._searchConfig) { return; } this.performSearch(this._searchConfig, true); } /** * @override */ jumpToPreviousSearchResult() { if (!this._searchResults || !this._searchConfig) { return; } this.performSearch(this._searchConfig, true, true); } /** * @override * @return {boolean} */ supportsCaseSensitiveSearch() { return false; } /** * @override * @return {boolean} */ supportsRegexSearch() { return false; } _highlightCurrentSearchResult() { const index = this._currentSearchResultIndex; const searchResults = this._searchResults; if (!searchResults) { return; } const searchResult = searchResults[index]; this._searchableView.updateCurrentMatchIndex(index); if (searchResult.node === null) { return; } if (typeof searchResult.node === 'undefined') { // No data for slot, request it. searchResult.domModel.searchResult(searchResult.index).then(node => { searchResult.node = node; // If any of these properties are undefined or reset to an invalid value, // this means the search/highlight request is outdated. const highlightRequestValid = this._searchConfig && this._searchResults && (this._currentSearchResultIndex !== -1); if (highlightRequestValid) { this._highlightCurrentSearchResult(); } }); return; } const treeElement = this._treeElementForNode(searchResult.node); searchResult.node.scrollIntoView(); if (treeElement) { this._searchConfig && treeElement.highlightSearchResults(this._searchConfig.query); treeElement.reveal(); const matches = treeElement.listItemElement.getElementsByClassName(UI.UIUtils.highlightedSearchResultClassName); if (matches.length) { matches[0].scrollIntoViewIfNeeded(false); } } } _hideSearchHighlights() { if (!this._searchResults || !this._searchResults.length || this._currentSearchResultIndex === -1) { return; } const searchResult = this._searchResults[this._currentSearchResultIndex]; if (!searchResult.node) { return; } const treeElement = this._treeElementForNode(searchResult.node); if (treeElement) { treeElement.hideSearchHighlights(); } } /** * @return {?SDK.DOMModel.DOMNode} */ selectedDOMNode() { for (const treeOutline of this._treeOutlines) { if (treeOutline.selectedDOMNode()) { return treeOutline.selectedDOMNode(); } } return null; } /** * @param {!SDK.DOMModel.DOMNode} node * @param {boolean=} focus */ selectDOMNode(node, focus) { for (const treeOutline of this._treeOutlines) { const outline = ElementsTreeOutline.forDOMModel(node.domModel()); if (outline === treeOutline) { treeOutline.selectDOMNode(node, focus); } else { treeOutline.selectDOMNode(null); } } } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _updateBreadcrumbIfNeeded(event) { const nodes = /** @type {!Array.<!SDK.DOMModel.DOMNode>} */ (event.data); /* If we don't have a selected node then we can tell the breadcrumbs that & bail. */ const selectedNode = this.selectedDOMNode(); if (!selectedNode) { this._breadcrumbs.data = { crumbs: [], selectedNode: null, }; return; } /* This function gets called whenever the tree outline is updated * and contains any nodes that have changed. * What we need to do is construct the new set of breadcrumb nodes, combining the Nodes * that we had before with the new nodes, and pass them into the breadcrumbs component. */ // Get the current set of active crumbs const activeNode = legacyNodeToNewBreadcrumbsNode(selectedNode); const existingCrumbs = [activeNode]; for (let current = selectedNode.parentNode; current; current = current.parentNode) { existingCrumbs.push(legacyNodeToNewBreadcrumbsNode(current)); } /* Get the change nodes from the event & convert them to breadcrumb nodes */ const newNodes = nodes.map(legacyNodeToNewBreadcrumbsNode); const nodesThatHaveChangedMap = new Map(); newNodes.forEach(crumb => nodesThatHaveChangedMap.set(crumb.id, crumb)); /* Loop over our existing crumbs, and if any have an ID that matches an ID from the new nodes * that we have, use the new node, rather than the one we had, because it's changed. */ const newSetOfCrumbs = existingCrumbs.map(crumb => { const replacement = nodesThatHaveChangedMap.get(crumb.id); return replacement || crumb; }); this._breadcrumbs.data = { crumbs: newSetOfCrumbs, selectedNode: activeNode, }; } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _crumbNodeSelected(event) { const node = /** @type {!SDK.DOMModel.DOMNode} */ (event.data); this.selectDOMNode(node, true); } /** * @param {?SDK.DOMModel.DOMNode} node * @return {?ElementsTreeOutline} */ _treeOutlineForNode(node) { if (!node) { return null; } return ElementsTreeOutline.forDOMModel(node.domModel()); } /** * @param {!SDK.DOMModel.DOMNode} node * @return {?ElementsTreeElement} */ _treeElementForNode(node) { const treeOutline = this._treeOutlineForNode(node); if (!treeOutline) { return null; } return /** @type {?ElementsTreeElement} */ (treeOutline.findTreeElement(node)); } /** * @param {!SDK.DOMModel.DOMNode} node * @return {!SDK.DOMModel.DOMNode} */ _leaveUserAgentShadowDOM(node) { let userAgentShadowRoot; while ((userAgentShadowRoot = node.ancestorUserAgentShadowRoot()) && userAgentShadowRoot.parentNode) { node = userAgentShadowRoot.parentNode; } return node; } /** * @param {!SDK.DOMModel.DOMNode} node * @param {boolean} focus * @param {boolean=} omitHighlight * @return {!Promise<void>} */ revealAndSelectNode(node, focus, omitHighlight) { this._omitDefaultSelection = true; node = Common.Settings.Settings.instance().moduleSetting('showUAShadowDOM').get() ? node : this._leaveUserAgentShadowDOM(node); if (!omitHighlight) { node.highlightForTwoSeconds(); } return UI.ViewManager.ViewManager.instance().showView('elements', false, !focus).then(() => { this.selectDOMNode(node, focus); delete this._omitDefaultSelection; if (!this._notFirstInspectElement) { ElementsPanel._firstInspectElementNodeNameForTest = node.nodeName(); ElementsPanel._firstInspectElementCompletedForTest(); Host.InspectorFrontendHost.InspectorFrontendHostInstance.inspectElementCompleted(); } this._notFirstInspectElement = true; }); } _showUAShadowDOMChanged() { for (const treeOutline of this._treeOutlines) { treeOutline.update(); } } /** * @param {!HTMLElement} stylePaneWrapperElement */ _setupTextSelectionHack(stylePaneWrapperElement) { // We "extend" the sidebar area when dragging, in order to keep smooth text // selection. It should be replaced by 'user-select: contain' in the future. const uninstallHackBound = uninstallHack.bind(this); // Fallback to cover unforeseen cases where text selection has ended. const uninstallHackOnMousemove = /** @param {!Event} event */ event => { if (/** @type {!MouseEvent} */ (event).buttons === 0) { uninstallHack.call(this); } }; stylePaneWrapperElement.addEventListener('mousedown', /** @param {!Event} event */ event => { if (/** @type {!MouseEvent} */ (event).button !== 0 /* left or main button */) { return; } this._splitWidget.element.classList.add('disable-resizer-for-elements-hack'); stylePaneWrapperElement.style.setProperty('height', `${stylePaneWrapperElement.offsetHeight}px`); const largeLength = 1000000; stylePaneWrapperElement.style.setProperty('left', `${- 1 * largeLength}px`); stylePaneWrapperElement.style.setProperty('padding-left', `${largeLength}px`); stylePaneWrapperElement.style.setProperty('width', `calc(100% + ${largeLength}px)`); stylePaneWrapperElement.style.setProperty('position', 'fixed'); stylePaneWrapperElement.window().addEventListener('blur', uninstallHackBound); stylePaneWrapperElement.window().addEventListener('contextmenu', uninstallHackBound, true); stylePaneWrapperElement.window().addEventListener('dragstart', uninstallHackBound, true); stylePaneWrapperElement.window().addEventListener('mousemove', uninstallHackOnMousemove, true); stylePaneWrapperElement.window().addEventListener('mouseup', uninstallHackBound, true); stylePaneWrapperElement.window().addEventListener('visibilitychange', uninstallHackBound); }, true); /** * @this {!ElementsPanel} */ function uninstallHack() { this._splitWidget.element.classList.remove('disable-resizer-for-elements-hack'); stylePaneWrapperElement.style.removeProperty('left'); stylePaneWrapperElement.style.removeProperty('padding-left'); stylePaneWrapperElement.style.removeProperty('width'); stylePaneWrapperElement.style.removeProperty('position'); stylePaneWrapperElement.window().removeEventListener('blur', uninstallHackBound); stylePaneWrapperElement.window().removeEventListener('contextmenu', uninstallHackBound, true); stylePaneWrapperElement.window().removeEventListener('dragstart', uninstallHackBound, true); stylePaneWrapperElement.window().removeEventListener('mousemove', uninstallHackOnMousemove, true); stylePaneWrapperElement.window().removeEventListener('mouseup', uninstallHackBound, true); stylePaneWrapperElement.window().removeEventListener('visibilitychange', uninstallHackBound); } } /** * @param {!_splitMode} splitMode */ _initializeSidebarPanes(splitMode) { this._splitWidget.setVertical(splitMode === _splitMode.Vertical); this.showToolbarPane(null /* widget */, null /* toggle */); const matchedStylePanesWrapper = new UI.Widget.VBox(); matchedStylePanesWrapper.element.classList.add('style-panes-wrapper'); this._stylesWidget.show(matchedStylePanesWrapper.element); this._setupTextSelectionHack(matchedStylePanesWrapper.element); const computedStylePanesWrapper = new UI.Widget.VBox(); computedStylePanesWrapper.element.classList.add('style-panes-wrapper'); this._computedStyleWidget.show(computedStylePanesWrapper.element); const stylesSplitWidget = new UI.SplitWidget.SplitWidget( true /* isVertical */, true /* secondIsSidebar */, 'elements.styles.sidebar.width', 100); stylesSplitWidget.setMainWidget(matchedStylePanesWrapper); stylesSplitWidget.hideSidebar(); stylesSplitWidget.enableShowModeSaving(); stylesSplitWidget.addEventListener(UI.SplitWidget.Events.ShowModeChanged, () => { showMetricsWidgetInStylesPane(); }); this._stylesWidget.addEventListener(StylesSidebarPaneEvents.InitialUpdateCompleted, () => { this._stylesWidget.appendToolbarItem( stylesSplitWidget.createShowHideSidebarButton(i18nString(UIStrings.computedStylesSidebar))); }); const showMetricsWidgetInComputedPane = () => { this._metricsWidget.show(computedStylePanesWrapper.element, this._computedStyleWidget.element); this._metricsWidget.toggleVisibility(true /* visible */); this._stylesWidget.removeEventListener(StylesSidebarPaneEvents.StylesUpdateCompleted, toggleMetricsWidget); }; const showMetricsWidgetInStylesPane = () => { const showMergedComputedPane = stylesSplitWidget.showMode() === UI.SplitWidget.ShowMode.Both; if (showMergedComputedPane) { showMetricsWidgetInComputedPane(); } else { this._metricsWidget.show(matchedStylePanesWrapper.element); if (!this._stylesWidget.hasMatchedStyles) { this._metricsWidget.toggleVisibility(false /* invisible */); } this._stylesWidget.addEventListener(StylesSidebarPaneEvents.StylesUpdateCompleted, toggleMetricsWidget); } }; let skippedInitialTabSelectedEvent = false; /** * @param {!Common.EventTarget.EventTargetEvent} event */ const toggleMetricsWidget = event => { this._metricsWidget.toggleVisibility(event.data.hasMatchedStyles); }; /** * @param {!Common.EventTarget.EventTargetEvent} event */ const tabSelected = event => { const tabId = /** @type {string} */ (event.data.tabId); if (tabId === i18nString(UIStrings.computed)) { computedStylePanesWrapper.show(computedView.element); showMetricsWidgetInComputedPane(); } else if (tabId === i18nString(UIStrings.styles)) { stylesSplitWidget.setSidebarWidget(computedStylePanesWrapper); showMetricsWidgetInStylesPane(); } if (skippedInitialTabSelectedEvent) { // We don't log the initially selected sidebar pane to UMA because // it will skew the histogram heavily toward the Styles pane Host.userMetrics.sidebarPaneShown(tabId); } else { skippedInitialTabSelectedEvent = true; } }; this.sidebarPaneView = UI.ViewManager.ViewManager.instance().createTabbedLocation( () => UI.ViewManager.ViewManager.instance().showView('elements')); const tabbedPane = this.sidebarPaneView.tabbedPane(); if (this._splitMode !== _splitMode.Vertical) { this._splitWidget.installResizer(tabbedPane.headerElement()); } const stylesView = new UI.View.SimpleView(i18nString(UIStrings.styles)); this.sidebarPaneView.appendView(stylesView); stylesView.element.classList.add('flex-auto'); stylesSplitWidget.show(stylesView.element); const computedView = new UI.View.SimpleView(i18nString(UIStrings.computed)); computedView.element.classList.add('composite', 'fill'); tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, tabSelected, this); this.sidebarPaneView.appendView(computedView); this._stylesViewToReveal = stylesView; this.sidebarPaneView.appendApplicableItems('elements-sidebar'); const extensionSidebarPanes = Extensions.ExtensionServer.ExtensionServer.instance().sidebarPanes(); for (let i = 0; i < extensionSidebarPanes.length; ++i) { this._addExtensionSidebarPane(extensionSidebarPanes[i]); } this._splitWidget.setSidebarWidget(this.sidebarPaneView.tabbedPane()); } _updateSidebarPosition() { if (this.sidebarPaneView && this.sidebarPaneView.tabbedPane().shouldHideOnDetach()) { return; } // We can't reparent extension iframes. const position = Common.Settings.Settings.instance().moduleSetting('sidebarPosition').get(); let splitMode = _splitMode.Horizontal; if (position === 'right' || (position === 'auto' && UI.InspectorView.InspectorView.instance().element.offsetWidth > 680)) { splitMode = _splitMode.Vertical; } if (!this.sidebarPaneView) { this._initializeSidebarPanes(splitMode); return; } if (splitMode === this._splitMode) { return; } this._splitMode = splitMode; const tabbedPane = this.sidebarPaneView.tabbedPane(); this._splitWidget.uninstallResizer(tabbedPane.headerElement()); this._splitWidget.setVertical(this._splitMode === _splitMode.Vertical); this.showToolbarPane(null /* widget */, null /* toggle */); if (this._splitMode !== _splitMode.Vertical) { this._splitWidget.installResizer(tabbedPane.headerElement()); } } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _extensionSidebarPaneAdded(event) { const pane = /** @type {!Extensions.ExtensionPanel.ExtensionSidebarPane} */ (event.data); this._addExtensionSidebarPane(pane); } /** * @param {!Extensions.ExtensionPanel.ExtensionSidebarPane} pane */ _addExtensionSidebarPane(pane) { if (this.sidebarPaneView && pane.panelName() === this.name) { this.sidebarPaneView.appendView(pane); } } /** * @param {!SDK.CSSModel.CSSModel} cssModel */ _setupStyleTracking(cssModel) { const gridStyleTracker = cssModel.createCSSPropertyTracker(TrackedCSSGridProperties); gridStyleTracker.start(); this._gridStyleTrackerByCSSModel.set(cssModel, gridStyleTracker); gridStyleTracker.addEventListener( SDK.CSSModel.CSSPropertyTrackerEvents.TrackedCSSPropertiesUpdated, this._trackedCSSPropertiesUpdated, this); } /** * @param {!SDK.CSSModel.CSSModel} cssModel */ _removeStyleTracking(cssModel) { const gridStyleTracker = this._gridStyleTrackerByCSSModel.get(cssModel); if (!gridStyleTracker) { return; } gridStyleTracker.stop(); this._gridStyleTrackerByCSSModel.delete(cssModel); gridStyleTracker.removeEventListener( SDK.CSSModel.CSSPropertyTrackerEvents.TrackedCSSPropertiesUpdated, this._trackedCSSPropertiesUpdated, this); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _trackedCSSPropertiesUpdated(event) { const domNodes = /** @type {!Array<?SDK.DOMModel.DOMNode>} */ (event.data.domNodes); for (const domNode of domNodes) { if (!domNode) { continue; } const treeElement = this._treeElementForNode(domNode); if (treeElement) { treeElement.updateStyleAdorners(); } } } } ElementsPanel._firstInspectElementCompletedForTest = function() {}; ElementsPanel._firstInspectElementNodeNameForTest = ''; /** @enum {symbol} */ export const _splitMode = { Vertical: Symbol('Vertical'), Horizontal: Symbol('Horizontal'), }; const TrackedCSSGridProperties = [ { name: 'display', value: 'grid', }, { name: 'display', value: 'inline-grid', }, { name: 'display', value: 'flex', }, { name: 'display', value: 'inline-flex', }, ]; /** @type {!ContextMenuProvider} */ let contextMenuProviderInstance; /** * @implements {UI.ContextMenu.Provider} */ export class ContextMenuProvider { /** * @override * @param {!Event} event * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {!Object} object */ appendApplicableItems(event, contextMenu, object) { if (!(object instanceof SDK.RemoteObject.RemoteObject && (/** @type {!SDK.RemoteObject.RemoteObject} */ (object)).isNode()) && !(object instanceof SDK.DOMModel.DOMNode) && !(object instanceof SDK.DOMModel.DeferredDOMNode)) { return; } // Skip adding "Reveal..." menu item for our own tree outline. if (ElementsPanel.instance().element.isAncestor(/** @type {!Node} */ (event.target))) { return; } /** @type {function():*} */ const commandCallback = Common.Revealer.reveal.bind(Common.Revealer.Revealer, object); contextMenu.revealSection().appendItem(i18nString(UIStrings.revealInElementsPanel), commandCallback); } static instance() { if (!contextMenuProviderInstance) { contextMenuProviderInstance = new ContextMenuProvider(); } return contextMenuProviderInstance; } } /** @type {DOMNodeRevealer} */ let dOMNodeRevealerInstance; /** * @implements {Common.Revealer.Revealer} */ export class DOMNodeRevealer { /** * @param {{forceNew: ?boolean}} opts */ static instance(opts = {forceNew: null}) { const {forceNew} = opts; if (!dOMNodeRevealerInstance || forceNew) { dOMNodeRevealerInstance = new DOMNodeRevealer(); } return dOMNodeRevealerInstance; } /** * @override * @param {!Object} node * @param {boolean=} omitFocus * @return {!Promise<void>} */ reveal(node, omitFocus) { const panel = ElementsPanel.instance(); panel._pendingNodeReveal = true; return new Promise(revealPromise); /** * @param {function():void} resolve * @param {function(!Error):void} reject */ function revealPromise(resolve, reject) { if (node instanceof SDK.DOMModel.DOMNode) { onNodeResolved(/** @type {!SDK.DOMModel.DOMNode} */ (node)); } else if (node instanceof SDK.DOMModel.DeferredDOMNode) { (/** @type {!SDK.DOMModel.DeferredDOMNode} */ (node)).resolve(checkDeferredDOMNodeThenReveal); } else if (node instanceof SDK.RemoteObject.RemoteObject) { const domModel = /** @type {!SDK.RemoteObject.RemoteObject} */ (node).runtimeModel().target().model(SDK.DOMModel.DOMModel); if (domModel) { domModel.pushObjectAsNodeToFrontend(node).then(checkRemoteObjectThenReveal); } else { reject(new Error('Could not resolve a node to reveal.')); } } else { reject(new Error('Can\'t reveal a non-node.')); panel._pendingNodeReveal = false; } /** * @param {!SDK.DOMModel.DOMNode} resolvedNode */ function onNodeResolved(resolvedNode) { panel._pendingNodeReveal = false; // A detached node could still have a parent and ownerDocument // properties, which means stepping up through the hierarchy to ensure // that the root node is the document itself. Any break implies // detachment. let currentNode = resolvedNode; while (currentNode.parentNode) { currentNode = currentNode.parentNode; } const isDetached = !(currentNode instanceof SDK.DOMModel.DOMDocument); const isDocument = node instanceof SDK.DOMModel.DOMDocument; if (!isDocument && isDetached) { const msg = i18nString(UIStrings.nodeCannotBeFoundInTheCurrent); Common.Console.Console.instance().warn(msg); reject(new Error(msg)); return; } if (resolvedNode) { panel.revealAndSelectNode(resolvedNode, !omitFocus).then(resolve); return; } reject(new Error('Could not resolve node to reveal.')); } /** * @param {?SDK.DOMModel.DOMNode} resolvedNode */ function checkRemoteObjectThenReveal(resolvedNode) { if (!resolvedNode) { const msg = i18nString(UIStrings.theRemoteObjectCouldNotBe); Common.Console.Console.instance().warn(msg); reject(new Error(msg)); return; } onNodeResolved(resolvedNode); } /** * @param {?SDK.DOMModel.DOMNode} resolvedNode */ function checkDeferredDOMNodeThenReveal(resolvedNode) { if (!resolvedNode) { const msg = i18nString(UIStrings.theDeferredDomNodeCouldNotBe); Common.Console.Console.instance().warn(msg); reject(new Error(msg)); return; } onNodeResolved(resolvedNode); } } } } /** @type {CSSPropertyRevealer} */ let cSSPropertyRevealerInstance; /** * @implements {Common.Revealer.Revealer} */ export class CSSPropertyRevealer { /** * @param {{forceNew: ?boolean}} opts */ static instance(opts = {forceNew: null}) { const {forceNew} = opts; if (!cSSPropertyRevealerInstance || forceNew) { cSSPropertyRevealerInstance = new CSSPropertyRevealer(); } return cSSPropertyRevealerInstance; } /** * @override * @param {!Object} property * @return {!Promise<void>} */ reveal(property) { const panel = ElementsPanel.instance(); return panel._revealProperty(/** @type {!SDK.CSSProperty.CSSProperty} */ (property)); } } /** @type {!ElementsActionDelegate} */ let elementsActionDelegateInstance; /** * @implements {UI.ActionRegistration.ActionDelegate} */ export class ElementsActionDelegate { /** * @override * @param {!UI.Context.Context} context * @param {string} actionId * @return {boolean} */ handleAction(context, actionId) { const node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode); if (!node) { return true; } const treeOutline = ElementsTreeOutline.forDOMModel(node.domModel()); if (!treeOutline) { return true; } switch (actionId) { case 'elements.hide-element': treeOutline.toggleHideElement(node); return true; case 'elements.edit-as-html': treeOutline.toggleEditAsHTML(node); return true; case 'elements.duplicate-element': treeOutline.duplicateNode(node); return true; case 'elements.undo': SDK.DOMModel.DOMModelUndoStack.instance().undo(); ElementsPanel.instance()._stylesWidget.forceUpdate(); return true; case 'elements.redo': SDK.DOMModel.DOMModelUndoStack.instance().redo(); ElementsPanel.instance()._stylesWidget.forceUpdate(); return true; } return false; } /** * @param {{forceNew: ?boolean}=} opts * @return {!ElementsActionDelegate} */ static instance(opts = {forceNew: null}) { const {forceNew} = opts; if (!elementsActionDelegateInstance || forceNew) { elementsActionDelegateInstance = new ElementsActionDelegate(); } return elementsActionDelegateInstance; } } /** @type {!PseudoStateMarkerDecorator} */ let pseudoStateMarkerDecoratorInstance; /** * @implements {MarkerDecorator} */ export class PseudoStateMarkerDecorator { /** * @param {{forceNew: ?boolean}} opts */ static instance(opts = {forceNew: null}) { const {forceNew} = opts; if (!pseudoStateMarkerDecoratorInstance || forceNew) { pseudoStateMarkerDecoratorInstance = new PseudoStateMarkerDecorator(); } return pseudoStateMarkerDecoratorInstance; } /** * @override * @param {!SDK.DOMModel.DOMNode} node * @return {?{title: string, color: string}} */ decorate(node) { const pseudoState = node.domModel().cssModel().pseudoState(node); if (!pseudoState) { return null; } return {color: 'orange', title: i18nString(UIStrings.elementStateS, {PH1: ':' + pseudoState.join(', :')})}; } }