mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-01 01:38:07 +08:00
This CL fixes the render flash of the metrics widget (box model) while changing inspected nodes in DevTools Elements tab. The issue is an inherent side effect of how the metrics widget and the matched styles widget are designed to render. They are separate widgets that are put together under the same parent widget, and no render timing is synced between. Whenever the inspected node is changed, they both will fire some CDP calls and re-render when the calls resolve. This means any one of them can be rendered first while the other waits for CDP calls, and usually the metrics widget wins. This issue did not exist before https://crrev.com/c/2562851 because it didn't clear previous matched styles sections while changing nodes. Now that previous styles sections are cleared, the metrics widget will re-render first while the styles section is empty, resulting in this render flash. This CL introduces a StylesUpdateCompleted event that fires when the Styles sections are re-populated, so that the render timing can be synced and thus ordered between the matched styles widget and the metrics widget. Fixed: chromium:1155456 Change-Id: I2f5d64dd046f39e483f7501a4b17d6358cd8d5df Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2577564 Commit-Queue: Changhao Han <changhaohan@chromium.org> Reviewed-by: Alex Rudenko <alexrudenko@chromium.org> Reviewed-by: Mathias Bynens <mathias@chromium.org>
1362 lines
45 KiB
JavaScript
1362 lines
45 KiB
JavaScript
/*
|
|
* 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 Root from '../root/root.js';
|
|
import * as SDK from '../sdk/sdk.js';
|
|
import * as UI from '../ui/ui.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';
|
|
|
|
/**
|
|
*
|
|
* @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(Common.UIString.UIString('Find by string, selector, or XPath'));
|
|
const stackElement = this._searchableView.element;
|
|
|
|
this._contentElement = document.createElement('div');
|
|
const crumbsContainer = document.createElement('div');
|
|
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';
|
|
|
|
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 {{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 = Common.UIString.UIString('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 (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(ls`Computed Styles sidebar`));
|
|
});
|
|
|
|
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 === Common.UIString.UIString('Computed')) {
|
|
computedStylePanesWrapper.show(computedView.element);
|
|
showMetricsWidgetInComputedPane();
|
|
} else if (tabId === Common.UIString.UIString('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(Common.UIString.UIString('Styles'));
|
|
this.sidebarPaneView.appendView(stylesView);
|
|
stylesView.element.classList.add('flex-auto');
|
|
stylesSplitWidget.show(stylesView.element);
|
|
|
|
const computedView = new UI.View.SimpleView(Common.UIString.UIString('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) {
|
|
if (Root.Runtime.experiments.isEnabled('cssGridFeatures')) {
|
|
// Style tracking is conditional on enabling experimental Grid features
|
|
// because it's the only use case for now.
|
|
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(Common.UIString.UIString('Reveal in Elements panel'), commandCallback);
|
|
}
|
|
|
|
static instance() {
|
|
if (!contextMenuProviderInstance) {
|
|
contextMenuProviderInstance = new ContextMenuProvider();
|
|
}
|
|
return contextMenuProviderInstance;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @implements {Common.Revealer.Revealer}
|
|
*/
|
|
export class DOMNodeRevealer {
|
|
/**
|
|
* @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 = ls`Node cannot be found in the current page.`;
|
|
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 = ls`The remote object could not be resolved into a valid node.`;
|
|
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 = ls`The deferred DOM Node could not be resolved into a valid node.`;
|
|
Common.Console.Console.instance().warn(msg);
|
|
reject(new Error(msg));
|
|
return;
|
|
}
|
|
onNodeResolved(resolvedNode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @implements {Common.Revealer.Revealer}
|
|
*/
|
|
export class CSSPropertyRevealer {
|
|
/**
|
|
* @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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @implements {MarkerDecorator}
|
|
*/
|
|
export class PseudoStateMarkerDecorator {
|
|
/**
|
|
* @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: Common.UIString.UIString('Element state: %s', ':' + pseudoState.join(', :'))};
|
|
}
|
|
}
|