mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-07 04:26:32 +08:00
This reverts commit9fd15ddbd0. Reason for revert: Fixed browser tests loading behavior Original change's description: > Revert "Migrate elements/ to ESM" > > This reverts commit8fefb2ca13. > > Reason for revert: broke browsertest https://ci.chromium.org/p/chromium/builders/try/linux-rel/250464 and blocking roll. > > Original change's description: > > Migrate elements/ to ESM > > > > Bug: 1006759 > > Change-Id: Ie9144a22562eb3ae3341a285f5ad16f3d630896c > > Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1934212 > > Reviewed-by: Paul Lewis <aerotwist@chromium.org> > > Commit-Queue: Paul Lewis <aerotwist@chromium.org> > > Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> > > TBR=aerotwist@chromium.org,tvanderlippe@chromium.org > > # Not skipping CQ checks because original CL landed > 1 day ago. > > Bug: 1006759 > Change-Id: I16a1fd9851391a6080c5376ca35866402b0bc445 > Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1937027 > Reviewed-by: Yang Guo <yangguo@chromium.org> > Commit-Queue: Yang Guo <yangguo@chromium.org> Change-Id: Iba84f5230f20a86b2de75b84115fcb10aeea534e Bug: 1006759 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1937029 Reviewed-by: Mathias Bynens <mathias@chromium.org> Commit-Queue: Mathias Bynens <mathias@chromium.org> Auto-Submit: Tim van der Lippe <tvanderlippe@chromium.org>
1137 lines
35 KiB
JavaScript
1137 lines
35 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.
|
|
*/
|
|
|
|
/**
|
|
* @implements {UI.Searchable}
|
|
* @implements {SDK.SDKModelObserver<!SDK.DOMModel>}
|
|
* @implements {UI.ViewLocationResolver}
|
|
* @unrestricted
|
|
*/
|
|
export default class ElementsPanel extends UI.Panel {
|
|
constructor() {
|
|
super('elements');
|
|
this.registerRequiredCSS('elements/elementsPanel.css');
|
|
|
|
this._splitWidget = new UI.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(this);
|
|
this._searchableView.setMinimumSize(25, 28);
|
|
this._searchableView.setPlaceholder(Common.UIString('Find by string, selector, or XPath'));
|
|
const stackElement = this._searchableView.element;
|
|
|
|
this._contentElement = createElement('div');
|
|
const crumbsContainer = 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.moduleSetting('domWordWrap').get()) {
|
|
this._contentElement.classList.add('elements-wrap');
|
|
}
|
|
Common.moduleSetting('domWordWrap').addChangeListener(this._domWordWrapSettingChanged.bind(this));
|
|
|
|
crumbsContainer.id = 'elements-crumbs';
|
|
this._breadcrumbs = new Elements.ElementsBreadcrumbs();
|
|
this._breadcrumbs.show(crumbsContainer);
|
|
this._breadcrumbs.addEventListener(Elements.ElementsBreadcrumbs.Events.NodeSelected, this._crumbNodeSelected, this);
|
|
|
|
this._stylesWidget = new Elements.StylesSidebarPane();
|
|
this._computedStyleWidget = new Elements.ComputedStyleWidget();
|
|
this._metricsWidget = new Elements.MetricsSidebarPane();
|
|
|
|
Common.moduleSetting('sidebarPosition').addChangeListener(this._updateSidebarPosition.bind(this));
|
|
this._updateSidebarPosition();
|
|
|
|
/** @type {!Array.<!Elements.ElementsTreeOutline>} */
|
|
this._treeOutlines = [];
|
|
/** @type {!Map<!Elements.ElementsTreeOutline, !Element>} */
|
|
this._treeOutlineHeaders = new Map();
|
|
SDK.targetManager.observeModels(SDK.DOMModel, this);
|
|
SDK.targetManager.addEventListener(
|
|
SDK.TargetManager.Events.NameChanged,
|
|
event => this._targetNameChanged(/** @type {!SDK.Target} */ (event.data)));
|
|
Common.moduleSetting('showUAShadowDOM').addChangeListener(this._showUAShadowDOMChanged.bind(this));
|
|
SDK.targetManager.addModelListener(
|
|
SDK.DOMModel, SDK.DOMModel.Events.DocumentUpdated, this._documentUpdatedEvent, this);
|
|
Extensions.extensionServer.addEventListener(
|
|
Extensions.ExtensionServer.Events.SidebarPaneAdded, this._extensionSidebarPaneAdded, this);
|
|
|
|
/**
|
|
* @type {!Array.<{domModel: !SDK.DOMModel, index: number, node: (?SDK.DOMNode|undefined)}>|undefined}
|
|
*/
|
|
this._searchResults;
|
|
}
|
|
|
|
/**
|
|
* @return {!ElementsPanel}
|
|
*/
|
|
static instance() {
|
|
return /** @type {!ElementsPanel} */ (self.runtime.sharedInstance(ElementsPanel));
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.CSSProperty} cssProperty
|
|
*/
|
|
_revealProperty(cssProperty) {
|
|
return this.sidebarPaneView.showView(this._stylesViewToReveal).then(() => {
|
|
this._stylesWidget.revealProperty(/** @type {!SDK.CSSProperty} */ (cssProperty));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {string} locationName
|
|
* @return {?UI.ViewLocation}
|
|
*/
|
|
resolveLocation(locationName) {
|
|
return this.sidebarPaneView;
|
|
}
|
|
|
|
/**
|
|
* @param {?UI.Widget} widget
|
|
* @param {?UI.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
|
|
*/
|
|
modelAdded(domModel) {
|
|
const parentModel = domModel.parentModel();
|
|
let treeOutline = parentModel ? Elements.ElementsTreeOutline.forDOMModel(parentModel) : null;
|
|
if (!treeOutline) {
|
|
treeOutline = new Elements.ElementsTreeOutline(true, true);
|
|
treeOutline.setWordWrap(Common.moduleSetting('domWordWrap').get());
|
|
treeOutline.addEventListener(
|
|
Elements.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedNodeChanged, this);
|
|
treeOutline.addEventListener(
|
|
Elements.ElementsTreeOutline.Events.ElementsTreeUpdated, this._updateBreadcrumbIfNeeded, this);
|
|
new Elements.ElementsTreeElementHighlighter(treeOutline);
|
|
this._treeOutlines.push(treeOutline);
|
|
if (domModel.target().parentTarget()) {
|
|
this._treeOutlineHeaders.set(treeOutline, createElementWithClass('div', 'elements-tree-header'));
|
|
this._targetNameChanged(domModel.target());
|
|
}
|
|
}
|
|
treeOutline.wireToDOMModel(domModel);
|
|
|
|
// Perform attach if necessary.
|
|
if (this.isShowing()) {
|
|
this.wasShown();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {!SDK.DOMModel} domModel
|
|
*/
|
|
modelRemoved(domModel) {
|
|
const treeOutline = Elements.ElementsTreeOutline.forDOMModel(domModel);
|
|
treeOutline.unwireFromDOMModel(domModel);
|
|
if (domModel.parentModel()) {
|
|
return;
|
|
}
|
|
this._treeOutlines.remove(treeOutline);
|
|
const header = this._treeOutlineHeaders.get(treeOutline);
|
|
if (header) {
|
|
header.remove();
|
|
}
|
|
this._treeOutlineHeaders.delete(treeOutline);
|
|
treeOutline.element.remove();
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.Target} target
|
|
*/
|
|
_targetNameChanged(target) {
|
|
const domModel = target.model(SDK.DOMModel);
|
|
if (!domModel) {
|
|
return;
|
|
}
|
|
const treeOutline = Elements.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('Frame');
|
|
header.appendChild(Components.Linkifier.linkifyURL(target.inspectedURL(), {text: target.name()}));
|
|
}
|
|
|
|
_updateTreeOutlineVisibleWidth() {
|
|
if (!this._treeOutlines.length) {
|
|
return;
|
|
}
|
|
|
|
let width = this._splitWidget.element.offsetWidth;
|
|
if (this._splitWidget.isVertical()) {
|
|
width -= this._splitWidget.sidebarSize();
|
|
}
|
|
for (let i = 0; i < this._treeOutlines.length; ++i) {
|
|
this._treeOutlines[i].setVisibleWidth(width);
|
|
}
|
|
|
|
this._breadcrumbs.updateSizes();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
focus() {
|
|
if (this._treeOutlines.length) {
|
|
this._treeOutlines[0].focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @return {!UI.SearchableView}
|
|
*/
|
|
searchableView() {
|
|
return this._searchableView;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
wasShown() {
|
|
UI.context.setFlavor(ElementsPanel, this);
|
|
|
|
for (let i = 0; i < this._treeOutlines.length; ++i) {
|
|
const treeOutline = this._treeOutlines[i];
|
|
// 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();
|
|
this._breadcrumbs.update();
|
|
|
|
const domModels = SDK.targetManager.models(SDK.DOMModel);
|
|
for (const domModel of domModels) {
|
|
if (domModel.parentModel()) {
|
|
continue;
|
|
}
|
|
const treeOutline = Elements.ElementsTreeOutline.forDOMModel(domModel);
|
|
treeOutline.setVisible(true);
|
|
|
|
if (!treeOutline.rootDOMNode) {
|
|
if (domModel.existingDocument()) {
|
|
treeOutline.rootDOMNode = domModel.existingDocument();
|
|
this._documentUpdated(domModel);
|
|
} else {
|
|
domModel.requestDocument();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
willHide() {
|
|
SDK.OverlayModel.hideDOMNodeHighlight();
|
|
for (let i = 0; i < this._treeOutlines.length; ++i) {
|
|
const treeOutline = this._treeOutlines[i];
|
|
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);
|
|
}
|
|
}
|
|
if (this._popoverHelper) {
|
|
this._popoverHelper.hidePopover();
|
|
}
|
|
super.willHide();
|
|
UI.context.setFlavor(ElementsPanel, null);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
onResize() {
|
|
this.element.window().requestAnimationFrame(this._updateSidebarPosition.bind(this)); // Do not force layout.
|
|
this._updateTreeOutlineVisibleWidth();
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Event} event
|
|
*/
|
|
_selectedNodeChanged(event) {
|
|
const selectedNode = /** @type {?SDK.DOMNode} */ (event.data.node);
|
|
const focus = /** @type {boolean} */ (event.data.focus);
|
|
for (const treeOutline of this._treeOutlines) {
|
|
if (!selectedNode || Elements.ElementsTreeOutline.forDOMModel(selectedNode.domModel()) !== treeOutline) {
|
|
treeOutline.selectDOMNode(null);
|
|
}
|
|
}
|
|
|
|
this._breadcrumbs.setSelectedNode(selectedNode);
|
|
|
|
UI.context.setFlavor(SDK.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.setFlavor(SDK.ExecutionContext, context);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Event} event
|
|
*/
|
|
_documentUpdatedEvent(event) {
|
|
const domModel = /** @type {!SDK.DOMModel} */ (event.data);
|
|
this._documentUpdated(domModel);
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.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);
|
|
|
|
/**
|
|
* @param {!SDK.DOMModel} domModel
|
|
* @param {?SDK.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;
|
|
}
|
|
this._setDefaultSelectedNode(node);
|
|
this._lastSelectedNodeSelectedForTest();
|
|
}
|
|
}
|
|
|
|
_lastSelectedNodeSelectedForTest() {
|
|
}
|
|
|
|
/**
|
|
* @param {?SDK.DOMNode} node
|
|
*/
|
|
_setDefaultSelectedNode(node) {
|
|
if (!node || this._hasNonDefaultSelectedNode || this._pendingNodeReveal) {
|
|
return;
|
|
}
|
|
const treeOutline = Elements.ElementsTreeOutline.forDOMModel(node.domModel());
|
|
if (!treeOutline) {
|
|
return;
|
|
}
|
|
this.selectDOMNode(node);
|
|
if (treeOutline.selectedTreeElement) {
|
|
treeOutline.selectedTreeElement.expand();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
searchCanceled() {
|
|
delete this._searchConfig;
|
|
this._hideSearchHighlights();
|
|
|
|
this._searchableView.updateSearchMatchesCount(0);
|
|
|
|
delete this._currentSearchResultIndex;
|
|
delete this._searchResults;
|
|
|
|
SDK.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.moduleSetting('showUAShadowDOM').get();
|
|
const domModels = SDK.targetManager.models(SDK.DOMModel);
|
|
const promises = domModels.map(domModel => domModel.performSearch(whitespaceTrimmedQuery, showUAShadowDOM));
|
|
Promise.all(promises).then(resultCountCallback.bind(this));
|
|
|
|
/**
|
|
* @param {!Array.<number>} resultCounts
|
|
* @this {ElementsPanel}
|
|
*/
|
|
function resultCountCallback(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 = undefined;
|
|
}
|
|
|
|
let index = this._currentSearchResultIndex;
|
|
|
|
if (shouldJump) {
|
|
if (this._currentSearchResultIndex === undefined) {
|
|
index = jumpBackwards ? -1 : 0;
|
|
} else {
|
|
index = jumpBackwards ? index - 1 : index + 1;
|
|
}
|
|
this._jumpToSearchResult(index);
|
|
}
|
|
}
|
|
}
|
|
|
|
_domWordWrapSettingChanged(event) {
|
|
// FIXME: crbug.com/425984
|
|
this._contentElement.classList.toggle('elements-wrap', event.data);
|
|
for (let i = 0; i < this._treeOutlines.length; ++i) {
|
|
this._treeOutlines[i].setWordWrap(/** @type {boolean} */ (event.data));
|
|
}
|
|
}
|
|
|
|
switchToAndFocus(node) {
|
|
// Reset search restore.
|
|
this._searchableView.cancelSearch();
|
|
UI.viewManager.showView('elements').then(() => this.selectDOMNode(node, true));
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @return {?UI.PopoverRequest}
|
|
*/
|
|
_getPopoverRequest(event) {
|
|
let link = event.target;
|
|
while (link && !link[Elements.ElementsTreeElement.HrefSymbol]) {
|
|
link = link.parentElementOrShadowHost();
|
|
}
|
|
if (!link) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
box: link.boxInWindow(),
|
|
show: async popover => {
|
|
const node = this.selectedDOMNode();
|
|
if (!node) {
|
|
return false;
|
|
}
|
|
const preview = await Components.ImagePreview.build(
|
|
node.domModel().target(), link[Elements.ElementsTreeElement.HrefSymbol], true);
|
|
if (preview) {
|
|
popover.contentElement.appendChild(preview);
|
|
}
|
|
return !!preview;
|
|
}
|
|
};
|
|
}
|
|
|
|
_jumpToSearchResult(index) {
|
|
if (!this._searchResults) {
|
|
return;
|
|
}
|
|
|
|
this._currentSearchResultIndex = (index + this._searchResults.length) % this._searchResults.length;
|
|
this._highlightCurrentSearchResult();
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
jumpToNextSearchResult() {
|
|
if (!this._searchResults) {
|
|
return;
|
|
}
|
|
this.performSearch(this._searchConfig, true);
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
*/
|
|
jumpToPreviousSearchResult() {
|
|
if (!this._searchResults) {
|
|
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;
|
|
this._highlightCurrentSearchResult();
|
|
});
|
|
return;
|
|
}
|
|
|
|
const treeElement = this._treeElementForNode(searchResult.node);
|
|
searchResult.node.scrollIntoView();
|
|
if (treeElement) {
|
|
treeElement.highlightSearchResults(this._searchConfig.query);
|
|
treeElement.reveal();
|
|
const matches = treeElement.listItemElement.getElementsByClassName(UI.highlightedSearchResultClassName);
|
|
if (matches.length) {
|
|
matches[0].scrollIntoViewIfNeeded(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
_hideSearchHighlights() {
|
|
if (!this._searchResults || !this._searchResults.length || this._currentSearchResultIndex === undefined) {
|
|
return;
|
|
}
|
|
const searchResult = this._searchResults[this._currentSearchResultIndex];
|
|
if (!searchResult.node) {
|
|
return;
|
|
}
|
|
const treeOutline = Elements.ElementsTreeOutline.forDOMModel(searchResult.node.domModel());
|
|
const treeElement = treeOutline.findTreeElement(searchResult.node);
|
|
if (treeElement) {
|
|
treeElement.hideSearchHighlights();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {?SDK.DOMNode}
|
|
*/
|
|
selectedDOMNode() {
|
|
for (let i = 0; i < this._treeOutlines.length; ++i) {
|
|
const treeOutline = this._treeOutlines[i];
|
|
if (treeOutline.selectedDOMNode()) {
|
|
return treeOutline.selectedDOMNode();
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} node
|
|
* @param {boolean=} focus
|
|
*/
|
|
selectDOMNode(node, focus) {
|
|
for (const treeOutline of this._treeOutlines) {
|
|
const outline = Elements.ElementsTreeOutline.forDOMModel(node.domModel());
|
|
if (outline === treeOutline) {
|
|
treeOutline.selectDOMNode(node, focus);
|
|
} else {
|
|
treeOutline.selectDOMNode(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Event} event
|
|
*/
|
|
_updateBreadcrumbIfNeeded(event) {
|
|
const nodes = /** @type {!Array.<!SDK.DOMNode>} */ (event.data);
|
|
this._breadcrumbs.updateNodes(nodes);
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Event} event
|
|
*/
|
|
_crumbNodeSelected(event) {
|
|
const node = /** @type {!SDK.DOMNode} */ (event.data);
|
|
this.selectDOMNode(node, true);
|
|
}
|
|
|
|
/**
|
|
* @param {?SDK.DOMNode} node
|
|
* @return {?Elements.ElementsTreeOutline}
|
|
*/
|
|
_treeOutlineForNode(node) {
|
|
if (!node) {
|
|
return null;
|
|
}
|
|
return Elements.ElementsTreeOutline.forDOMModel(node.domModel());
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} node
|
|
* @return {?Elements.ElementsTreeElement}
|
|
*/
|
|
_treeElementForNode(node) {
|
|
const treeOutline = this._treeOutlineForNode(node);
|
|
return /** @type {?Elements.ElementsTreeElement} */ (treeOutline.findTreeElement(node));
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} node
|
|
* @return {!SDK.DOMNode}
|
|
*/
|
|
_leaveUserAgentShadowDOM(node) {
|
|
let userAgentShadowRoot;
|
|
while ((userAgentShadowRoot = node.ancestorUserAgentShadowRoot()) && userAgentShadowRoot.parentNode) {
|
|
node = userAgentShadowRoot.parentNode;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* @param {!SDK.DOMNode} node
|
|
* @param {boolean} focus
|
|
* @param {boolean=} omitHighlight
|
|
* @return {!Promise}
|
|
*/
|
|
revealAndSelectNode(node, focus, omitHighlight) {
|
|
this._omitDefaultSelection = true;
|
|
|
|
node = Common.moduleSetting('showUAShadowDOM').get() ? node : this._leaveUserAgentShadowDOM(node);
|
|
if (!omitHighlight) {
|
|
node.highlightForTwoSeconds();
|
|
}
|
|
|
|
return UI.viewManager.showView('elements', false, !focus).then(() => {
|
|
this.selectDOMNode(node, focus);
|
|
delete this._omitDefaultSelection;
|
|
|
|
if (!this._notFirstInspectElement) {
|
|
Elements.ElementsPanel._firstInspectElementNodeNameForTest = node.nodeName();
|
|
Elements.ElementsPanel._firstInspectElementCompletedForTest();
|
|
Host.InspectorFrontendHost.inspectElementCompleted();
|
|
}
|
|
this._notFirstInspectElement = true;
|
|
});
|
|
}
|
|
|
|
_showUAShadowDOMChanged() {
|
|
for (let i = 0; i < this._treeOutlines.length; ++i) {
|
|
this._treeOutlines[i].update();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} 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 = event => {
|
|
if (event.buttons === 0) {
|
|
uninstallHack.call(this);
|
|
}
|
|
};
|
|
|
|
stylePaneWrapperElement.addEventListener('mousedown', event => {
|
|
if (event.which !== 1) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
_updateSidebarPosition() {
|
|
if (this.sidebarPaneView && this.sidebarPaneView.tabbedPane().shouldHideOnDetach()) {
|
|
return;
|
|
} // We can't reparent extension iframes.
|
|
|
|
let splitMode;
|
|
const position = Common.moduleSetting('sidebarPosition').get();
|
|
if (position === 'right' || (position === 'auto' && UI.inspectorView.element.offsetWidth > 680)) {
|
|
splitMode = _splitMode.Vertical;
|
|
} else if (UI.inspectorView.element.offsetWidth > 415) {
|
|
splitMode = _splitMode.Horizontal;
|
|
} else {
|
|
splitMode = _splitMode.Slim;
|
|
}
|
|
|
|
if (this.sidebarPaneView && splitMode === this._splitMode) {
|
|
return;
|
|
}
|
|
this._splitMode = splitMode;
|
|
|
|
const extensionSidebarPanes = Extensions.extensionServer.sidebarPanes();
|
|
let lastSelectedTabId = null;
|
|
if (this.sidebarPaneView) {
|
|
lastSelectedTabId = this.sidebarPaneView.tabbedPane().selectedTabId;
|
|
this.sidebarPaneView.tabbedPane().detach();
|
|
this._splitWidget.uninstallResizer(this.sidebarPaneView.tabbedPane().headerElement());
|
|
}
|
|
|
|
this._splitWidget.setVertical(this._splitMode === _splitMode.Vertical);
|
|
this.showToolbarPane(null /* widget */, null /* toggle */);
|
|
|
|
const matchedStylePanesWrapper = new UI.VBox();
|
|
matchedStylePanesWrapper.element.classList.add('style-panes-wrapper');
|
|
this._stylesWidget.show(matchedStylePanesWrapper.element);
|
|
this._setupTextSelectionHack(matchedStylePanesWrapper.element);
|
|
|
|
const computedStylePanesWrapper = new UI.VBox();
|
|
computedStylePanesWrapper.element.classList.add('style-panes-wrapper');
|
|
this._computedStyleWidget.show(computedStylePanesWrapper.element);
|
|
|
|
/**
|
|
* @param {boolean} inComputedStyle
|
|
* @this {ElementsPanel}
|
|
*/
|
|
function showMetrics(inComputedStyle) {
|
|
if (inComputedStyle) {
|
|
this._metricsWidget.show(computedStylePanesWrapper.element, this._computedStyleWidget.element);
|
|
} else {
|
|
this._metricsWidget.show(matchedStylePanesWrapper.element);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Event} event
|
|
* @this {ElementsPanel}
|
|
*/
|
|
function tabSelected(event) {
|
|
const tabId = /** @type {string} */ (event.data.tabId);
|
|
if (tabId === Common.UIString('Computed')) {
|
|
showMetrics.call(this, true);
|
|
} else if (tabId === Common.UIString('Styles')) {
|
|
showMetrics.call(this, false);
|
|
}
|
|
}
|
|
|
|
this.sidebarPaneView = UI.viewManager.createTabbedLocation(() => UI.viewManager.showView('elements'));
|
|
const tabbedPane = this.sidebarPaneView.tabbedPane();
|
|
if (this._popoverHelper) {
|
|
this._popoverHelper.hidePopover();
|
|
}
|
|
this._popoverHelper = new UI.PopoverHelper(tabbedPane.element, this._getPopoverRequest.bind(this));
|
|
this._popoverHelper.setHasPadding(true);
|
|
this._popoverHelper.setTimeout(0);
|
|
|
|
if (this._splitMode !== _splitMode.Vertical) {
|
|
this._splitWidget.installResizer(tabbedPane.headerElement());
|
|
}
|
|
|
|
const stylesView = new UI.SimpleView(Common.UIString('Styles'));
|
|
this.sidebarPaneView.appendView(stylesView);
|
|
if (splitMode === _splitMode.Horizontal) {
|
|
// Styles and computed are merged into a single tab.
|
|
stylesView.element.classList.add('flex-auto');
|
|
|
|
const splitWidget = new UI.SplitWidget(true, true, 'stylesPaneSplitViewState', 215);
|
|
splitWidget.show(stylesView.element);
|
|
splitWidget.setMainWidget(matchedStylePanesWrapper);
|
|
splitWidget.setSidebarWidget(computedStylePanesWrapper);
|
|
} else {
|
|
// Styles and computed are in separate tabs.
|
|
stylesView.element.classList.add('flex-auto');
|
|
matchedStylePanesWrapper.show(stylesView.element);
|
|
|
|
const computedView = new UI.SimpleView(Common.UIString('Computed'));
|
|
computedView.element.classList.add('composite', 'fill');
|
|
computedStylePanesWrapper.show(computedView.element);
|
|
|
|
tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, tabSelected, this);
|
|
this.sidebarPaneView.appendView(computedView);
|
|
}
|
|
this._stylesViewToReveal = stylesView;
|
|
|
|
showMetrics.call(this, this._splitMode === _splitMode.Horizontal);
|
|
|
|
this.sidebarPaneView.appendApplicableItems('elements-sidebar');
|
|
for (let i = 0; i < extensionSidebarPanes.length; ++i) {
|
|
this._addExtensionSidebarPane(extensionSidebarPanes[i]);
|
|
}
|
|
|
|
if (lastSelectedTabId) {
|
|
this.sidebarPaneView.tabbedPane().selectTab(lastSelectedTabId);
|
|
}
|
|
|
|
this._splitWidget.setSidebarWidget(this.sidebarPaneView.tabbedPane());
|
|
}
|
|
|
|
/**
|
|
* @param {!Common.Event} event
|
|
*/
|
|
_extensionSidebarPaneAdded(event) {
|
|
const pane = /** @type {!Extensions.ExtensionSidebarPane} */ (event.data);
|
|
this._addExtensionSidebarPane(pane);
|
|
}
|
|
|
|
/**
|
|
* @param {!Extensions.ExtensionSidebarPane} pane
|
|
*/
|
|
_addExtensionSidebarPane(pane) {
|
|
if (pane.panelName() === this.name) {
|
|
this.sidebarPaneView.appendView(pane);
|
|
}
|
|
}
|
|
}
|
|
|
|
export const _elementsSidebarViewTitleSymbol = Symbol('title');
|
|
|
|
/** @enum {symbol} */
|
|
export const _splitMode = {
|
|
Vertical: Symbol('Vertical'),
|
|
Horizontal: Symbol('Horizontal'),
|
|
Slim: Symbol('Slim'),
|
|
};
|
|
|
|
/**
|
|
* @implements {UI.ContextMenu.Provider}
|
|
* @unrestricted
|
|
*/
|
|
export class ContextMenuProvider {
|
|
/**
|
|
* @override
|
|
* @param {!Event} event
|
|
* @param {!UI.ContextMenu} contextMenu
|
|
* @param {!Object} object
|
|
*/
|
|
appendApplicableItems(event, contextMenu, object) {
|
|
if (!(object instanceof SDK.RemoteObject && (/** @type {!SDK.RemoteObject} */ (object)).isNode()) &&
|
|
!(object instanceof SDK.DOMNode) && !(object instanceof SDK.DeferredDOMNode)) {
|
|
return;
|
|
}
|
|
|
|
// Skip adding "Reveal..." menu item for our own tree outline.
|
|
if (ElementsPanel.instance().element.isAncestor(/** @type {!Node} */ (event.target))) {
|
|
return;
|
|
}
|
|
const commandCallback = Common.Revealer.reveal.bind(Common.Revealer, object);
|
|
contextMenu.revealSection().appendItem(Common.UIString('Reveal in Elements panel'), commandCallback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @implements {Common.Revealer}
|
|
* @unrestricted
|
|
*/
|
|
export class DOMNodeRevealer {
|
|
/**
|
|
* @override
|
|
* @param {!Object} node
|
|
* @param {boolean=} omitFocus
|
|
* @return {!Promise}
|
|
*/
|
|
reveal(node, omitFocus) {
|
|
const panel = ElementsPanel.instance();
|
|
panel._pendingNodeReveal = true;
|
|
|
|
return new Promise(revealPromise);
|
|
|
|
/**
|
|
* @param {function(undefined)} resolve
|
|
* @param {function(!Error)} reject
|
|
*/
|
|
function revealPromise(resolve, reject) {
|
|
if (node instanceof SDK.DOMNode) {
|
|
onNodeResolved(/** @type {!SDK.DOMNode} */ (node));
|
|
} else if (node instanceof SDK.DeferredDOMNode) {
|
|
(/** @type {!SDK.DeferredDOMNode} */ (node)).resolve(onNodeResolved);
|
|
} else if (node instanceof SDK.RemoteObject) {
|
|
const domModel = /** @type {!SDK.RemoteObject} */ (node).runtimeModel().target().model(SDK.DOMModel);
|
|
if (domModel) {
|
|
domModel.pushObjectAsNodeToFrontend(node).then(onNodeResolved);
|
|
} 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.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.DOMDocument);
|
|
|
|
const isDocument = node instanceof SDK.DOMDocument;
|
|
if (!isDocument && isDetached) {
|
|
const msg = ls`Node cannot be found in the current page.`;
|
|
Common.console.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.'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @implements {Common.Revealer}
|
|
* @unrestricted
|
|
*/
|
|
export class CSSPropertyRevealer {
|
|
/**
|
|
* @override
|
|
* @param {!Object} property
|
|
* @return {!Promise}
|
|
*/
|
|
reveal(property) {
|
|
const panel = ElementsPanel.instance();
|
|
return panel._revealProperty(/** @type {!SDK.CSSProperty} */ (property));
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @implements {UI.ActionDelegate}
|
|
* @unrestricted
|
|
*/
|
|
export class ElementsActionDelegate {
|
|
/**
|
|
* @override
|
|
* @param {!UI.Context} context
|
|
* @param {string} actionId
|
|
* @return {boolean}
|
|
*/
|
|
handleAction(context, actionId) {
|
|
const node = UI.context.flavor(SDK.DOMNode);
|
|
if (!node) {
|
|
return true;
|
|
}
|
|
const treeOutline = Elements.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.undo':
|
|
SDK.domModelUndoStack.undo();
|
|
ElementsPanel.instance()._stylesWidget.forceUpdate();
|
|
return true;
|
|
case 'elements.redo':
|
|
SDK.domModelUndoStack.redo();
|
|
ElementsPanel.instance()._stylesWidget.forceUpdate();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @implements {Elements.MarkerDecorator}
|
|
* @unrestricted
|
|
*/
|
|
export class PseudoStateMarkerDecorator {
|
|
/**
|
|
* @override
|
|
* @param {!SDK.DOMNode} node
|
|
* @return {?{title: string, color: string}}
|
|
*/
|
|
decorate(node) {
|
|
return {
|
|
color: 'orange',
|
|
title: Common.UIString('Element state: %s', ':' + node.domModel().cssModel().pseudoState(node).join(', :'))
|
|
};
|
|
}
|
|
}
|
|
|
|
/* Legacy exported object */
|
|
self.Elements = self.Elements || {};
|
|
|
|
/* Legacy exported object */
|
|
Elements = Elements || {};
|
|
|
|
/** @constructor */
|
|
Elements.ElementsPanel = ElementsPanel;
|
|
|
|
// Sniffed in tests.
|
|
Elements.ElementsPanel._firstInspectElementCompletedForTest = function() {};
|
|
|
|
Elements.ElementsPanel._elementsSidebarViewTitleSymbol = _elementsSidebarViewTitleSymbol;
|
|
|
|
/** @enum {symbol} */
|
|
Elements.ElementsPanel._splitMode = _splitMode;
|
|
|
|
/** @constructor */
|
|
Elements.ElementsPanel.ContextMenuProvider = ContextMenuProvider;
|
|
|
|
/** @constructor */
|
|
Elements.ElementsPanel.DOMNodeRevealer = DOMNodeRevealer;
|
|
|
|
/** @constructor */
|
|
Elements.ElementsPanel.CSSPropertyRevealer = CSSPropertyRevealer;
|
|
|
|
/** @constructor */
|
|
Elements.ElementsActionDelegate = ElementsActionDelegate;
|
|
|
|
/** @constructor */
|
|
Elements.ElementsPanel.PseudoStateMarkerDecorator = PseudoStateMarkerDecorator;
|