| // Copyright 2021 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /* |
| * 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 '../../core/common/common.js'; |
| import * as Host from '../../core/host/host.js'; |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import * as Platform from '../../core/platform/platform.js'; |
| import * as Root from '../../core/root/root.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import * as Extensions from '../../models/extensions/extensions.js'; |
| |
| import elementsPanelStyles from './elementsPanel.css.js'; |
| |
| import type * as Adorners from '../../ui/components/adorners/adorners.js'; |
| import * as Buttons from '../../ui/components/buttons/buttons.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| import * as TreeOutline from '../../ui/components/tree_outline/tree_outline.js'; |
| |
| import {AccessibilityTreeView} from './AccessibilityTreeView.js'; |
| import {type AXTreeNodeData} from './AccessibilityTreeUtils.js'; |
| import * as ElementsComponents from './components/components.js'; |
| import {ComputedStyleWidget} from './ComputedStyleWidget.js'; |
| |
| import {type ElementsTreeElement} from './ElementsTreeElement.js'; |
| import {ElementsTreeElementHighlighter} from './ElementsTreeElementHighlighter.js'; |
| import {ElementsTreeOutline} from './ElementsTreeOutline.js'; |
| import {type MarkerDecorator} from './MarkerDecorator.js'; |
| import {MetricsSidebarPane} from './MetricsSidebarPane.js'; |
| import {LayoutSidebarPane} from './LayoutSidebarPane.js'; |
| import { |
| Events as StylesSidebarPaneEvents, |
| StylesSidebarPane, |
| type StylesUpdateCompletedEvent, |
| } from './StylesSidebarPane.js'; |
| import {ColorSwatchPopoverIcon} from './ColorSwatchPopoverIcon.js'; |
| |
| const UIStrings = { |
| /** |
| * @description Placeholder text for the search box the Elements Panel. Selector refers to CSS |
| * selectors. |
| */ |
| findByStringSelectorOrXpath: 'Find by string, selector, or `XPath`', |
| /** |
| * @description Button text for a button that takes the user to the Accessibility Tree View from the |
| * DOM tree view, in the Elements panel. |
| */ |
| switchToAccessibilityTreeView: 'Switch to Accessibility Tree view', |
| /** |
| * @description Button text for a button that takes the user to the DOM tree view from the |
| * Accessibility Tree View, in the Elements panel. |
| */ |
| switchToDomTreeView: 'Switch to DOM Tree view', |
| /** |
| * @description Tooltip for the the Computed Styles sidebar toggle in the Styles pane. Command to |
| * open/show the sidebar. |
| */ |
| showComputedStylesSidebar: 'Show Computed Styles sidebar', |
| /** |
| * @description Tooltip for the the Computed Styles sidebar toggle in the Styles pane. Command to |
| * close/hide the sidebar. |
| */ |
| hideComputedStylesSidebar: 'Hide Computed Styles sidebar', |
| /** |
| * @description Screen reader announcement when the computed styles sidebar is shown in the Elements panel. |
| */ |
| computedStylesShown: 'Computed Styles sidebar shown', |
| /** |
| * @description Screen reader announcement when the computed styles sidebar is hidden in the Elements panel. |
| */ |
| computedStylesHidden: 'Computed Styles sidebar hidden', |
| /** |
| * @description Title of a pane in the Elements panel that shows computed styles for the selected |
| * HTML element. Computed styles are the final, actual styles of the element, including all |
| * implicit and specified styles. |
| */ |
| computed: 'Computed', |
| /** |
| * @description Title of a pane in the Elements panel that shows the CSS styles for the selected |
| * HTML element. |
| */ |
| 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 Warning/error text displayed when a node cannot be found in the current 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. A remote |
| * object is a JavaScript object that is not stored in DevTools, that DevTools has a connection to. |
| * It should correspond to a local node. |
| */ |
| theRemoteObjectCouldNotBe: 'The remote object could not be resolved to a valid node.', |
| /** |
| * @description Console warning when the user tries to reveal a deferred DOM Node that resolves as |
| * null. A deferred DOM node is a node we know about but have not yet fetched from the backend (we |
| * defer the work until later). |
| */ |
| theDeferredDomNodeCouldNotBe: 'The deferred `DOM` Node could not be resolved to a valid node.', |
| /** |
| * @description Text in Elements Panel of the Elements panel. Shows the current CSS Pseudo-classes |
| * applicable to the selected HTML element. |
| * @example {::after, ::before} PH1 |
| */ |
| elementStateS: 'Element state: {PH1}', |
| /** |
| * @description Accessible name for side panel toolbar. |
| */ |
| sidePanelToolbar: 'Side panel toolbar', |
| /** |
| * @description Accessible name for side panel contents. |
| */ |
| sidePanelContent: 'Side panel content', |
| /** |
| * @description Accessible name for the DOM tree explorer view. |
| */ |
| domTreeExplorer: 'DOM tree explorer', |
| }; |
| |
| const str_ = i18n.i18n.registerUIStrings('panels/elements/ElementsPanel.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| |
| /** |
| * These strings need to match the `SidebarPaneCodes` in UserMetrics.ts. DevTools |
| * collects usage metrics for the different sidebar tabs. |
| */ |
| export const enum SidebarPaneTabId { |
| Computed = 'Computed', |
| Styles = 'Styles', |
| } |
| |
| const createAccessibilityTreeToggleButton = (isActive: boolean): HTMLElement => { |
| const button = new Buttons.Button.Button(); |
| const title = |
| isActive ? i18nString(UIStrings.switchToDomTreeView) : i18nString(UIStrings.switchToAccessibilityTreeView); |
| button.data = { |
| active: isActive, |
| variant: Buttons.Button.Variant.TOOLBAR, |
| iconUrl: new URL('../../Images/person.svg', import.meta.url).toString(), |
| title, |
| }; |
| button.tabIndex = 0; |
| button.classList.add('axtree-button'); |
| if (isActive) { |
| button.classList.add('active'); |
| } |
| return button; |
| }; |
| |
| let elementsPanelInstance: ElementsPanel; |
| |
| export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.Searchable, |
| SDK.TargetManager.SDKModelObserver<SDK.DOMModel.DOMModel>, |
| UI.View.ViewLocationResolver { |
| private splitWidget: UI.SplitWidget.SplitWidget; |
| private readonly searchableViewInternal: UI.SearchableView.SearchableView; |
| private mainContainer: HTMLDivElement; |
| private domTreeContainer: HTMLDivElement; |
| private splitMode: _splitMode|null; |
| private readonly accessibilityTreeView: AccessibilityTreeView|undefined; |
| private breadcrumbs: ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs; |
| stylesWidget: StylesSidebarPane; |
| private readonly computedStyleWidget: ComputedStyleWidget; |
| private readonly metricsWidget: MetricsSidebarPane; |
| private treeOutlines: Set<ElementsTreeOutline> = new Set(); |
| private searchResults!: { |
| domModel: SDK.DOMModel.DOMModel, |
| index: number, |
| node: ((SDK.DOMModel.DOMNode | undefined)|null), |
| }[]|undefined; |
| private currentSearchResultIndex: number; |
| pendingNodeReveal: boolean; |
| private readonly adornerManager: ElementsComponents.AdornerManager.AdornerManager; |
| private adornerSettingsPane: ElementsComponents.AdornerSettingsPane.AdornerSettingsPane|null; |
| private readonly adornersByName: Map<string, Set<Adorners.Adorner.Adorner>>; |
| accessibilityTreeButton?: HTMLElement; |
| domTreeButton?: HTMLElement; |
| private selectedNodeOnReset?: SDK.DOMModel.DOMNode; |
| private hasNonDefaultSelectedNode?: boolean; |
| private searchConfig?: UI.SearchableView.SearchConfig; |
| private omitDefaultSelection?: boolean; |
| private notFirstInspectElement?: boolean; |
| sidebarPaneView?: UI.View.TabbedViewLocation; |
| private stylesViewToReveal?: UI.View.SimpleView; |
| |
| private cssStyleTrackerByCSSModel: Map<SDK.CSSModel.CSSModel, SDK.CSSModel.CSSPropertyTracker>; |
| |
| constructor() { |
| super('elements'); |
| |
| 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.searchableViewInternal = new UI.SearchableView.SearchableView(this, null); |
| this.searchableViewInternal.setMinimumSize(25, 28); |
| this.searchableViewInternal.setPlaceholder(i18nString(UIStrings.findByStringSelectorOrXpath)); |
| const stackElement = this.searchableViewInternal.element; |
| |
| this.mainContainer = document.createElement('div'); |
| this.domTreeContainer = document.createElement('div'); |
| const crumbsContainer = document.createElement('div'); |
| if (Root.Runtime.experiments.isEnabled('fullAccessibilityTree')) { |
| this.initializeFullAccessibilityTreeView(); |
| } |
| this.mainContainer.appendChild(this.domTreeContainer); |
| stackElement.appendChild(this.mainContainer); |
| stackElement.appendChild(crumbsContainer); |
| |
| UI.ARIAUtils.markAsMain(this.domTreeContainer); |
| UI.ARIAUtils.setAccessibleName(this.domTreeContainer, i18nString(UIStrings.domTreeExplorer)); |
| |
| this.splitWidget.setMainWidget(this.searchableViewInternal); |
| this.splitMode = null; |
| |
| this.mainContainer.id = 'main-content'; |
| this.domTreeContainer.id = 'elements-content'; |
| this.domTreeContainer.tabIndex = -1; |
| // FIXME: crbug.com/425984 |
| if (Common.Settings.Settings.instance().moduleSetting('domWordWrap').get()) { |
| this.domTreeContainer.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, new TreeOutline.TreeOutline.TreeOutline<AXTreeNodeData>()); |
| } |
| this.breadcrumbs = new ElementsComponents.ElementsBreadcrumbs.ElementsBreadcrumbs(); |
| this.breadcrumbs.addEventListener('breadcrumbsnodeselected', event => { |
| this.crumbNodeSelected(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(); |
| |
| this.cssStyleTrackerByCSSModel = new Map(); |
| SDK.TargetManager.TargetManager.instance().observeModels(SDK.DOMModel.DOMModel, this, {scoped: true}); |
| SDK.TargetManager.TargetManager.instance().addEventListener( |
| SDK.TargetManager.Events.NameChanged, event => this.targetNameChanged(event.data)); |
| Common.Settings.Settings.instance() |
| .moduleSetting('showUAShadowDOM') |
| .addChangeListener(this.showUAShadowDOMChanged.bind(this)); |
| Extensions.ExtensionServer.ExtensionServer.instance().addEventListener( |
| Extensions.ExtensionServer.Events.SidebarPaneAdded, this.extensionSidebarPaneAdded, this); |
| this.currentSearchResultIndex = -1; // -1 represents the initial invalid state |
| |
| this.pendingNodeReveal = false; |
| |
| this.adornerManager = new ElementsComponents.AdornerManager.AdornerManager( |
| Common.Settings.Settings.instance().moduleSetting('adornerSettings')); |
| this.adornerSettingsPane = null; |
| this.adornersByName = new Map(); |
| } |
| |
| private initializeFullAccessibilityTreeView(): void { |
| this.accessibilityTreeButton = createAccessibilityTreeToggleButton(false); |
| this.accessibilityTreeButton.addEventListener('click', this.showAccessibilityTree.bind(this)); |
| |
| this.domTreeButton = createAccessibilityTreeToggleButton(true); |
| this.domTreeButton.addEventListener('click', this.showDOMTree.bind(this)); |
| |
| this.mainContainer.appendChild(this.accessibilityTreeButton); |
| } |
| |
| private showAccessibilityTree(): void { |
| if (this.accessibilityTreeView) { |
| this.splitWidget.setMainWidget(this.accessibilityTreeView); |
| } |
| } |
| |
| private showDOMTree(): void { |
| this.splitWidget.setMainWidget(this.searchableViewInternal); |
| const selectedNode = this.selectedDOMNode(); |
| if (!selectedNode) { |
| return; |
| } |
| const treeElement = this.treeElementForNode(selectedNode); |
| if (!treeElement) { |
| return; |
| } |
| treeElement.select(); |
| } |
| |
| static instance(opts: { |
| forceNew: boolean|null, |
| }|undefined = {forceNew: null}): ElementsPanel { |
| const {forceNew} = opts; |
| if (!elementsPanelInstance || forceNew) { |
| elementsPanelInstance = new ElementsPanel(); |
| } |
| |
| return elementsPanelInstance; |
| } |
| |
| revealProperty(cssProperty: SDK.CSSProperty.CSSProperty): Promise<void> { |
| if (!this.sidebarPaneView || !this.stylesViewToReveal) { |
| return Promise.resolve(); |
| } |
| |
| return this.sidebarPaneView.showView(this.stylesViewToReveal).then(() => { |
| this.stylesWidget.revealProperty((cssProperty as SDK.CSSProperty.CSSProperty)); |
| }); |
| } |
| |
| resolveLocation(_locationName: string): UI.View.ViewLocation|null { |
| return this.sidebarPaneView || null; |
| } |
| |
| showToolbarPane(widget: UI.Widget.Widget|null, toggle: UI.Toolbar.ToolbarToggle|null): void { |
| // TODO(luoe): remove this function once its providers have an alternative way to reveal their views. |
| this.stylesWidget.showToolbarPane(widget, toggle); |
| } |
| |
| modelAdded(domModel: SDK.DOMModel.DOMModel): void { |
| const parentModel = domModel.parentModel(); |
| |
| let treeOutline: ElementsTreeOutline|null = 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, new Common.Throttler.Throttler(100)); |
| this.treeOutlines.add(treeOutline); |
| } |
| treeOutline.wireToDOMModel(domModel); |
| |
| this.setupStyleTracking(domModel.cssModel()); |
| |
| // Perform attach if necessary. |
| if (this.isShowing()) { |
| this.wasShown(); |
| } |
| if (this.domTreeContainer.hasFocus()) { |
| treeOutline.focus(); |
| } |
| domModel.addEventListener(SDK.DOMModel.Events.DocumentUpdated, this.documentUpdatedEvent, this); |
| } |
| |
| modelRemoved(domModel: SDK.DOMModel.DOMModel): void { |
| domModel.removeEventListener(SDK.DOMModel.Events.DocumentUpdated, this.documentUpdatedEvent, this); |
| const treeOutline = ElementsTreeOutline.forDOMModel(domModel); |
| if (!treeOutline) { |
| return; |
| } |
| |
| treeOutline.unwireFromDOMModel(domModel); |
| if (domModel.parentModel()) { |
| return; |
| } |
| this.treeOutlines.delete(treeOutline); |
| treeOutline.element.remove(); |
| |
| this.removeStyleTracking(domModel.cssModel()); |
| } |
| |
| private targetNameChanged(target: SDK.Target.Target): void { |
| const domModel = target.model(SDK.DOMModel.DOMModel); |
| if (!domModel) { |
| return; |
| } |
| const treeOutline = ElementsTreeOutline.forDOMModel(domModel); |
| if (!treeOutline) { |
| return; |
| } |
| } |
| |
| private updateTreeOutlineVisibleWidth(): void { |
| 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(): void { |
| if (this.treeOutlines.size) { |
| this.treeOutlines.values().next().value.focus(); |
| } else { |
| this.domTreeContainer.focus(); |
| } |
| } |
| |
| override searchableView(): UI.SearchableView.SearchableView { |
| return this.searchableViewInternal; |
| } |
| |
| override wasShown(): void { |
| super.wasShown(); |
| UI.Context.Context.instance().setFlavor(ElementsPanel, this); |
| this.registerCSSFiles([elementsPanelStyles]); |
| |
| for (const treeOutline of this.treeOutlines) { |
| // Attach heavy component lazily |
| if (treeOutline.element.parentElement !== this.domTreeContainer) { |
| this.domTreeContainer.appendChild(treeOutline.element); |
| } |
| } |
| |
| const domModels = SDK.TargetManager.TargetManager.instance().models(SDK.DOMModel.DOMModel, {scoped: true}); |
| 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 { |
| void domModel.requestDocument(); |
| } |
| } |
| } |
| } |
| |
| override willHide(): void { |
| SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); |
| for (const treeOutline of this.treeOutlines) { |
| treeOutline.setVisible(false); |
| // Detach heavy component on hide |
| this.domTreeContainer.removeChild(treeOutline.element); |
| } |
| super.willHide(); |
| UI.Context.Context.instance().setFlavor(ElementsPanel, null); |
| } |
| |
| override onResize(): void { |
| this.element.window().requestAnimationFrame(this.updateSidebarPosition.bind(this)); // Do not force layout. |
| this.updateTreeOutlineVisibleWidth(); |
| } |
| |
| private selectedNodeChanged( |
| event: Common.EventTarget.EventTargetEvent<{node: SDK.DOMModel.DOMNode | null, focus: boolean}>): void { |
| let selectedNode = 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} = event.data; |
| for (const treeOutline of this.treeOutlines) { |
| if (!selectedNode || ElementsTreeOutline.forDOMModel(selectedNode.domModel()) !== treeOutline) { |
| treeOutline.selectDOMNode(null); |
| } |
| } |
| |
| if (selectedNode) { |
| const activeNode = ElementsComponents.Helper.legacyNodeToElementsComponentsNode(selectedNode); |
| const crumbs = [activeNode]; |
| |
| for (let current: (SDK.DOMModel.DOMNode|null) = selectedNode.parentNode; current; current = current.parentNode) { |
| crumbs.push(ElementsComponents.Helper.legacyNodeToElementsComponentsNode(current)); |
| } |
| |
| this.breadcrumbs.data = { |
| crumbs, |
| selectedNode: ElementsComponents.Helper.legacyNodeToElementsComponentsNode(selectedNode), |
| }; |
| |
| if (this.accessibilityTreeView) { |
| void this.accessibilityTreeView.selectedNodeChanged(selectedNode); |
| } |
| } else { |
| this.breadcrumbs.data = {crumbs: [], selectedNode: null}; |
| } |
| |
| UI.Context.Context.instance().setFlavor(SDK.DOMModel.DOMNode, selectedNode); |
| |
| if (!selectedNode) { |
| return; |
| } |
| void 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; |
| } |
| } |
| } |
| |
| private documentUpdatedEvent(event: Common.EventTarget.EventTargetEvent<SDK.DOMModel.DOMModel>): void { |
| const domModel = event.data; |
| this.documentUpdated(domModel); |
| this.removeStyleTracking(domModel.cssModel()); |
| this.setupStyleTracking(domModel.cssModel()); |
| } |
| |
| private documentUpdated(domModel: SDK.DOMModel.DOMModel): void { |
| this.searchableViewInternal.cancelSearch(); |
| |
| if (!domModel.existingDocument()) { |
| if (this.isShowing()) { |
| void domModel.requestDocument(); |
| } |
| return; |
| } |
| |
| this.hasNonDefaultSelectedNode = false; |
| |
| if (this.omitDefaultSelection) { |
| return; |
| } |
| |
| const savedSelectedNodeOnReset = this.selectedNodeOnReset; |
| void restoreNode.call(this, domModel, this.selectedNodeOnReset || null); |
| |
| async function restoreNode( |
| this: ElementsPanel, domModel: SDK.DOMModel.DOMModel, staleNode: SDK.DOMModel.DOMNode|null): Promise<void> { |
| const nodePath = staleNode ? staleNode.path() : null; |
| const restoredNodeId = nodePath ? await domModel.pushNodeByPathToFrontend(nodePath) : null; |
| |
| if (savedSelectedNodeOnReset !== this.selectedNodeOnReset) { |
| return; |
| } |
| let node: (SDK.DOMModel.DOMNode|null) = 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(); |
| } |
| } |
| } |
| |
| private lastSelectedNodeSelectedForTest(): void { |
| } |
| |
| private setDefaultSelectedNode(node: SDK.DOMModel.DOMNode|null): void { |
| 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(); |
| } |
| } |
| |
| onSearchClosed(): void { |
| const selectedNode = this.selectedDOMNode(); |
| if (!selectedNode) { |
| return; |
| } |
| const treeElement = this.treeElementForNode(selectedNode); |
| if (!treeElement) { |
| return; |
| } |
| |
| treeElement.select(); |
| } |
| |
| onSearchCanceled(): void { |
| this.searchConfig = undefined; |
| this.hideSearchHighlights(); |
| |
| this.searchableViewInternal.updateSearchMatchesCount(0); |
| |
| this.currentSearchResultIndex = -1; |
| delete this.searchResults; |
| |
| SDK.DOMModel.DOMModel.cancelSearch(); |
| } |
| |
| performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void { |
| const query = searchConfig.query; |
| |
| const whitespaceTrimmedQuery = query.trim(); |
| if (!whitespaceTrimmedQuery.length) { |
| return; |
| } |
| |
| if (!this.searchConfig || this.searchConfig.query !== query) { |
| this.onSearchCanceled(); |
| } else { |
| this.hideSearchHighlights(); |
| } |
| |
| this.searchConfig = searchConfig; |
| |
| const showUAShadowDOM = Common.Settings.Settings.instance().moduleSetting('showUAShadowDOM').get(); |
| const domModels = SDK.TargetManager.TargetManager.instance().models(SDK.DOMModel.DOMModel, {scoped: true}); |
| const promises = domModels.map(domModel => domModel.performSearch(whitespaceTrimmedQuery, showUAShadowDOM)); |
| void 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.searchableViewInternal.updateSearchMatchesCount(this.searchResults.length); |
| if (!this.searchResults.length) { |
| return; |
| } |
| if (this.currentSearchResultIndex >= this.searchResults.length) { |
| this.currentSearchResultIndex = -1; |
| } |
| |
| let index: (0|- 1)|number = this.currentSearchResultIndex; |
| |
| if (shouldJump) { |
| if (this.currentSearchResultIndex === -1) { |
| index = jumpBackwards ? -1 : 0; |
| } else { |
| index = jumpBackwards ? index - 1 : index + 1; |
| } |
| this.jumpToSearchResult(index); |
| } |
| }); |
| } |
| |
| private domWordWrapSettingChanged(event: Common.EventTarget.EventTargetEvent<boolean>): void { |
| this.domTreeContainer.classList.toggle('elements-wrap', event.data); |
| for (const treeOutline of this.treeOutlines) { |
| treeOutline.setWordWrap(event.data); |
| } |
| } |
| |
| switchToAndFocus(node: SDK.DOMModel.DOMNode): void { |
| // Reset search restore. |
| this.searchableViewInternal.cancelSearch(); |
| void UI.ViewManager.ViewManager.instance().showView('elements').then(() => this.selectDOMNode(node, true)); |
| } |
| |
| private jumpToSearchResult(index: number): void { |
| if (!this.searchResults) { |
| return; |
| } |
| |
| this.currentSearchResultIndex = (index + this.searchResults.length) % this.searchResults.length; |
| this.highlightCurrentSearchResult(); |
| } |
| |
| jumpToNextSearchResult(): void { |
| if (!this.searchResults || !this.searchConfig) { |
| return; |
| } |
| this.performSearch(this.searchConfig, true); |
| } |
| |
| jumpToPreviousSearchResult(): void { |
| if (!this.searchResults || !this.searchConfig) { |
| return; |
| } |
| this.performSearch(this.searchConfig, true, true); |
| } |
| |
| supportsCaseSensitiveSearch(): boolean { |
| return false; |
| } |
| |
| supportsRegexSearch(): boolean { |
| return false; |
| } |
| |
| private highlightCurrentSearchResult(): void { |
| const index = this.currentSearchResultIndex; |
| const searchResults = this.searchResults; |
| if (!searchResults) { |
| return; |
| } |
| const searchResult = searchResults[index]; |
| |
| this.searchableViewInternal.updateCurrentMatchIndex(index); |
| if (searchResult.node === null) { |
| return; |
| } |
| |
| if (typeof searchResult.node === 'undefined') { |
| // No data for slot, request it. |
| void 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); |
| void 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); |
| } |
| treeElement.select(/* omitFocus */ true); |
| } |
| } |
| |
| private hideSearchHighlights(): void { |
| 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(); |
| } |
| } |
| |
| selectedDOMNode(): SDK.DOMModel.DOMNode|null { |
| for (const treeOutline of this.treeOutlines) { |
| if (treeOutline.selectedDOMNode()) { |
| return treeOutline.selectedDOMNode(); |
| } |
| } |
| return null; |
| } |
| |
| selectDOMNode(node: SDK.DOMModel.DOMNode, focus?: boolean): void { |
| for (const treeOutline of this.treeOutlines) { |
| const outline = ElementsTreeOutline.forDOMModel(node.domModel()); |
| if (outline === treeOutline) { |
| treeOutline.selectDOMNode(node, focus); |
| } else { |
| treeOutline.selectDOMNode(null); |
| } |
| } |
| } |
| |
| selectAndShowSidebarTab(tabId: SidebarPaneTabId): void { |
| if (!this.sidebarPaneView) { |
| return; |
| } |
| |
| this.sidebarPaneView.tabbedPane().selectTab(tabId); |
| |
| if (!this.isShowing()) { |
| void UI.ViewManager.ViewManager.instance().showView('elements'); |
| } |
| } |
| |
| private updateBreadcrumbIfNeeded(event: Common.EventTarget.EventTargetEvent<SDK.DOMModel.DOMNode[]>): void { |
| const nodes = 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 = ElementsComponents.Helper.legacyNodeToElementsComponentsNode(selectedNode); |
| const existingCrumbs = [activeNode]; |
| for (let current: (SDK.DOMModel.DOMNode|null) = selectedNode.parentNode; current; current = current.parentNode) { |
| existingCrumbs.push(ElementsComponents.Helper.legacyNodeToElementsComponentsNode(current)); |
| } |
| |
| /* Get the change nodes from the event & convert them to breadcrumb nodes */ |
| const newNodes = nodes.map(ElementsComponents.Helper.legacyNodeToElementsComponentsNode); |
| const nodesThatHaveChangedMap = new Map<number, ElementsComponents.Helper.DOMNode>(); |
| 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, |
| }; |
| } |
| |
| private crumbNodeSelected(event: ElementsComponents.ElementsBreadcrumbs.NodeSelectedEvent): void { |
| this.selectDOMNode(event.legacyDomNode, true); |
| } |
| |
| private treeOutlineForNode(node: SDK.DOMModel.DOMNode|null): ElementsTreeOutline|null { |
| if (!node) { |
| return null; |
| } |
| return ElementsTreeOutline.forDOMModel(node.domModel()); |
| } |
| |
| private treeElementForNode(node: SDK.DOMModel.DOMNode): ElementsTreeElement|null { |
| const treeOutline = this.treeOutlineForNode(node); |
| if (!treeOutline) { |
| return null; |
| } |
| return treeOutline.findTreeElement(node); |
| } |
| |
| private leaveUserAgentShadowDOM(node: SDK.DOMModel.DOMNode): SDK.DOMModel.DOMNode { |
| let userAgentShadowRoot; |
| while ((userAgentShadowRoot = node.ancestorUserAgentShadowRoot()) && userAgentShadowRoot.parentNode) { |
| node = userAgentShadowRoot.parentNode; |
| } |
| return node; |
| } |
| |
| async revealAndSelectNode(nodeToReveal: SDK.DOMModel.DOMNode, focus: boolean, omitHighlight?: boolean): |
| Promise<void> { |
| this.omitDefaultSelection = true; |
| |
| const node = Common.Settings.Settings.instance().moduleSetting('showUAShadowDOM').get() ? |
| nodeToReveal : |
| this.leaveUserAgentShadowDOM(nodeToReveal); |
| if (!omitHighlight) { |
| node.highlightForTwoSeconds(); |
| } |
| |
| if (this.accessibilityTreeView) { |
| void this.accessibilityTreeView.revealAndSelectNode(nodeToReveal); |
| } |
| |
| await UI.ViewManager.ViewManager.instance().showView('elements', false, !focus); |
| this.selectDOMNode(node, focus); |
| delete this.omitDefaultSelection; |
| if (!this.notFirstInspectElement) { |
| ElementsPanel.firstInspectElementNodeNameForTest = node.nodeName(); |
| ElementsPanel.firstInspectElementCompletedForTest(); |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.inspectElementCompleted(); |
| } |
| this.notFirstInspectElement = true; |
| } |
| |
| private showUAShadowDOMChanged(): void { |
| for (const treeOutline of this.treeOutlines) { |
| treeOutline.update(); |
| } |
| } |
| |
| private setupTextSelectionHack(stylePaneWrapperElement: HTMLElement): void { |
| // 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: Event): void => { |
| if ((event as MouseEvent).buttons === 0) { |
| uninstallHack.call(this); |
| } |
| }; |
| |
| stylePaneWrapperElement.addEventListener('mousedown', (event: Event) => { |
| if ((event as MouseEvent).button !== 0) { |
| 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); |
| |
| function uninstallHack(this: ElementsPanel): void { |
| 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); |
| } |
| } |
| |
| private initializeSidebarPanes(splitMode: _splitMode): void { |
| 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.showComputedStylesSidebar), i18nString(UIStrings.hideComputedStylesSidebar), |
| i18nString(UIStrings.computedStylesShown), i18nString(UIStrings.computedStylesHidden))); |
| }); |
| |
| const showMetricsWidgetInComputedPane = (): void => { |
| this.metricsWidget.show(computedStylePanesWrapper.element, this.computedStyleWidget.element); |
| this.metricsWidget.toggleVisibility(true /* visible */); |
| this.stylesWidget.removeEventListener(StylesSidebarPaneEvents.StylesUpdateCompleted, toggleMetricsWidget); |
| }; |
| |
| const showMetricsWidgetInStylesPane = (): void => { |
| 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; |
| |
| const toggleMetricsWidget = (event: Common.EventTarget.EventTargetEvent<StylesUpdateCompletedEvent>): void => { |
| this.metricsWidget.toggleVisibility(event.data.hasMatchedStyles); |
| }; |
| |
| const tabSelected = (event: Common.EventTarget.EventTargetEvent<UI.TabbedPane.EventData>): void => { |
| const {tabId} = event.data; |
| if (tabId === SidebarPaneTabId.Computed) { |
| computedStylePanesWrapper.show(computedView.element); |
| showMetricsWidgetInComputedPane(); |
| } else if (tabId === SidebarPaneTabId.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'), 'Styles-pane-sidebar', false, true); |
| const tabbedPane = this.sidebarPaneView.tabbedPane(); |
| if (this.splitMode !== _splitMode.Vertical) { |
| this.splitWidget.installResizer(tabbedPane.headerElement()); |
| } |
| |
| const headerElement = tabbedPane.headerElement(); |
| UI.ARIAUtils.markAsNavigation(headerElement); |
| UI.ARIAUtils.setAccessibleName(headerElement, i18nString(UIStrings.sidePanelToolbar)); |
| |
| const contentElement = tabbedPane.tabbedPaneContentElement(); |
| UI.ARIAUtils.markAsComplementary(contentElement); |
| UI.ARIAUtils.setAccessibleName(contentElement, i18nString(UIStrings.sidePanelContent)); |
| |
| const stylesView = |
| new UI.View.SimpleView(i18nString(UIStrings.styles), /* isWebComponent */ undefined, SidebarPaneTabId.Styles); |
| this.sidebarPaneView.appendView(stylesView); |
| stylesView.element.classList.add('flex-auto'); |
| stylesSplitWidget.show(stylesView.element); |
| |
| const computedView = new UI.View.SimpleView( |
| i18nString(UIStrings.computed), /* isWebComponent */ undefined, SidebarPaneTabId.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()); |
| } |
| |
| private updateSidebarPosition(): void { |
| 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()); |
| } |
| } |
| |
| private extensionSidebarPaneAdded( |
| event: Common.EventTarget.EventTargetEvent<Extensions.ExtensionPanel.ExtensionSidebarPane>): void { |
| this.addExtensionSidebarPane(event.data); |
| } |
| |
| private addExtensionSidebarPane(pane: Extensions.ExtensionPanel.ExtensionSidebarPane): void { |
| if (this.sidebarPaneView && pane.panelName() === this.name) { |
| this.sidebarPaneView.appendView(pane); |
| } |
| } |
| |
| getComputedStyleWidget(): ComputedStyleWidget { |
| return this.computedStyleWidget; |
| } |
| |
| private setupStyleTracking(cssModel: SDK.CSSModel.CSSModel): void { |
| const cssPropertyTracker = cssModel.createCSSPropertyTracker(TrackedCSSProperties); |
| cssPropertyTracker.start(); |
| this.cssStyleTrackerByCSSModel.set(cssModel, cssPropertyTracker); |
| cssPropertyTracker.addEventListener( |
| SDK.CSSModel.CSSPropertyTrackerEvents.TrackedCSSPropertiesUpdated, this.trackedCSSPropertiesUpdated, this); |
| } |
| |
| private removeStyleTracking(cssModel: SDK.CSSModel.CSSModel): void { |
| const cssPropertyTracker = this.cssStyleTrackerByCSSModel.get(cssModel); |
| if (!cssPropertyTracker) { |
| return; |
| } |
| |
| cssPropertyTracker.stop(); |
| this.cssStyleTrackerByCSSModel.delete(cssModel); |
| cssPropertyTracker.removeEventListener( |
| SDK.CSSModel.CSSPropertyTrackerEvents.TrackedCSSPropertiesUpdated, this.trackedCSSPropertiesUpdated, this); |
| } |
| |
| private trackedCSSPropertiesUpdated({data: domNodes}: |
| Common.EventTarget.EventTargetEvent<(SDK.DOMModel.DOMNode | null)[]>): void { |
| for (const domNode of domNodes) { |
| if (!domNode) { |
| continue; |
| } |
| const treeElement = this.treeElementForNode(domNode); |
| if (treeElement) { |
| void treeElement.updateStyleAdorners(); |
| } |
| |
| LayoutSidebarPane.instance().update(); |
| } |
| } |
| |
| showAdornerSettingsPane(): void { |
| // Delay the initialization of the pane to the first showing |
| // since usually this pane won't be used. |
| if (!this.adornerSettingsPane) { |
| this.adornerSettingsPane = new ElementsComponents.AdornerSettingsPane.AdornerSettingsPane(); |
| this.adornerSettingsPane.addEventListener('adornersettingupdated', (event: Event) => { |
| const {adornerName, isEnabledNow, newSettings} = |
| (event as ElementsComponents.AdornerSettingsPane.AdornerSettingUpdatedEvent).data; |
| const adornersToUpdate = this.adornersByName.get(adornerName); |
| if (adornersToUpdate) { |
| for (const adorner of adornersToUpdate) { |
| isEnabledNow ? adorner.show() : adorner.hide(); |
| } |
| } |
| this.adornerManager.updateSettings(newSettings); |
| }); |
| this.searchableViewInternal.element.prepend(this.adornerSettingsPane); |
| } |
| |
| const adornerSettings = this.adornerManager.getSettings(); |
| this.adornerSettingsPane.data = { |
| settings: adornerSettings, |
| }; |
| this.adornerSettingsPane.show(); |
| } |
| |
| isAdornerEnabled(adornerText: string): boolean { |
| return this.adornerManager.isAdornerEnabled(adornerText); |
| } |
| |
| registerAdorner(adorner: Adorners.Adorner.Adorner): void { |
| let adornerSet = this.adornersByName.get(adorner.name); |
| if (!adornerSet) { |
| adornerSet = new Set(); |
| this.adornersByName.set(adorner.name, adornerSet); |
| } |
| adornerSet.add(adorner); |
| if (!this.isAdornerEnabled(adorner.name)) { |
| adorner.hide(); |
| } |
| } |
| |
| deregisterAdorner(adorner: Adorners.Adorner.Adorner): void { |
| const adornerSet = this.adornersByName.get(adorner.name); |
| if (!adornerSet) { |
| return; |
| } |
| adornerSet.delete(adorner); |
| } |
| |
| private static firstInspectElementCompletedForTest = function(): void {}; |
| private static firstInspectElementNodeNameForTest = ''; |
| } |
| |
| // @ts-ignore exported for Tests.js |
| globalThis.Elements = globalThis.Elements || {}; |
| // @ts-ignore exported for Tests.js |
| globalThis.Elements.ElementsPanel = ElementsPanel; |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| export const enum _splitMode { |
| Vertical = 'Vertical', |
| Horizontal = 'Horizontal', |
| } |
| |
| const TrackedCSSProperties = [ |
| { |
| name: 'display', |
| value: 'grid', |
| }, |
| { |
| name: 'display', |
| value: 'inline-grid', |
| }, |
| { |
| name: 'display', |
| value: 'flex', |
| }, |
| { |
| name: 'display', |
| value: 'inline-flex', |
| }, |
| { |
| name: 'container-type', |
| value: 'inline-size', |
| }, |
| { |
| name: 'container-type', |
| value: 'block-size', |
| }, |
| { |
| name: 'container-type', |
| value: 'size', |
| }, |
| ]; |
| |
| let contextMenuProviderInstance: ContextMenuProvider; |
| |
| export class ContextMenuProvider implements UI.ContextMenu.Provider { |
| appendApplicableItems(event: Event, contextMenu: UI.ContextMenu.ContextMenu, object: Object): void { |
| if (!(object instanceof SDK.RemoteObject.RemoteObject && (object as SDK.RemoteObject.RemoteObject).isNode()) && |
| !(object instanceof SDK.DOMModel.DOMNode) && !(object instanceof SDK.DOMModel.DeferredDOMNode)) { |
| return; |
| } |
| if (ElementsPanel.instance().element.isAncestor((event.target as Node))) { |
| return; |
| } |
| const commandCallback: () => void = Common.Revealer.reveal.bind(Common.Revealer.Revealer, object); |
| contextMenu.revealSection().appendItem(i18nString(UIStrings.revealInElementsPanel), commandCallback); |
| } |
| |
| static instance(): ContextMenuProvider { |
| if (!contextMenuProviderInstance) { |
| contextMenuProviderInstance = new ContextMenuProvider(); |
| } |
| return contextMenuProviderInstance; |
| } |
| } |
| let dOMNodeRevealerInstance: DOMNodeRevealer; |
| export class DOMNodeRevealer implements Common.Revealer.Revealer { |
| static instance(opts: { |
| forceNew: boolean|null, |
| } = {forceNew: null}): DOMNodeRevealer { |
| const {forceNew} = opts; |
| if (!dOMNodeRevealerInstance || forceNew) { |
| dOMNodeRevealerInstance = new DOMNodeRevealer(); |
| } |
| |
| return dOMNodeRevealerInstance; |
| } |
| |
| reveal(node: Object, omitFocus?: boolean): Promise<void> { |
| const panel = ElementsPanel.instance(); |
| panel.pendingNodeReveal = true; |
| |
| return (new Promise<void>(revealPromise)).catch((reason: Error) => { |
| let message: string; |
| if (Platform.UserVisibleError.isUserVisibleError(reason)) { |
| message = reason.message; |
| } else { |
| message = i18nString(UIStrings.nodeCannotBeFoundInTheCurrent); |
| } |
| |
| Common.Console.Console.instance().warn(message); |
| // Blink tests expect an exception to be raised and unhandled here to detect that the node |
| // was actually not successfully viewed. |
| throw reason; |
| }); |
| |
| function revealPromise( |
| resolve: () => void, reject: (arg0: Platform.UserVisibleError.UserVisibleError) => void): void { |
| if (node instanceof SDK.DOMModel.DOMNode) { |
| onNodeResolved((node as SDK.DOMModel.DOMNode)); |
| } else if (node instanceof SDK.DOMModel.DeferredDOMNode) { |
| (node as SDK.DOMModel.DeferredDOMNode).resolve(checkDeferredDOMNodeThenReveal); |
| } else if (node instanceof SDK.RemoteObject.RemoteObject) { |
| const domModel = node.runtimeModel().target().model(SDK.DOMModel.DOMModel); |
| if (domModel) { |
| void domModel.pushObjectAsNodeToFrontend(node).then(checkRemoteObjectThenReveal); |
| } else { |
| const msg = i18nString(UIStrings.nodeCannotBeFoundInTheCurrent); |
| reject(new Platform.UserVisibleError.UserVisibleError(msg)); |
| } |
| } else { |
| const msg = i18nString(UIStrings.theRemoteObjectCouldNotBe); |
| reject(new Platform.UserVisibleError.UserVisibleError(msg)); |
| panel.pendingNodeReveal = false; |
| } |
| |
| function onNodeResolved(resolvedNode: SDK.DOMModel.DOMNode): void { |
| 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: SDK.DOMModel.DOMNode = 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); |
| reject(new Platform.UserVisibleError.UserVisibleError(msg)); |
| return; |
| } |
| |
| if (resolvedNode) { |
| void panel.revealAndSelectNode(resolvedNode, !omitFocus).then(resolve); |
| return; |
| } |
| const msg = i18nString(UIStrings.nodeCannotBeFoundInTheCurrent); |
| reject(new Platform.UserVisibleError.UserVisibleError(msg)); |
| } |
| |
| function checkRemoteObjectThenReveal(resolvedNode: SDK.DOMModel.DOMNode|null): void { |
| if (!resolvedNode) { |
| const msg = i18nString(UIStrings.theRemoteObjectCouldNotBe); |
| reject(new Platform.UserVisibleError.UserVisibleError(msg)); |
| return; |
| } |
| onNodeResolved(resolvedNode); |
| } |
| |
| function checkDeferredDOMNodeThenReveal(resolvedNode: SDK.DOMModel.DOMNode|null): void { |
| if (!resolvedNode) { |
| const msg = i18nString(UIStrings.theDeferredDomNodeCouldNotBe); |
| reject(new Platform.UserVisibleError.UserVisibleError(msg)); |
| return; |
| } |
| onNodeResolved(resolvedNode); |
| } |
| } |
| } |
| } |
| |
| let cSSPropertyRevealerInstance: CSSPropertyRevealer; |
| |
| export class CSSPropertyRevealer implements Common.Revealer.Revealer { |
| static instance(opts: { |
| forceNew: boolean|null, |
| } = {forceNew: null}): CSSPropertyRevealer { |
| const {forceNew} = opts; |
| if (!cSSPropertyRevealerInstance || forceNew) { |
| cSSPropertyRevealerInstance = new CSSPropertyRevealer(); |
| } |
| |
| return cSSPropertyRevealerInstance; |
| } |
| |
| reveal(property: Object): Promise<void> { |
| const panel = ElementsPanel.instance(); |
| return panel.revealProperty((property as SDK.CSSProperty.CSSProperty)); |
| } |
| } |
| |
| let elementsActionDelegateInstance: ElementsActionDelegate; |
| |
| export class ElementsActionDelegate implements UI.ActionRegistration.ActionDelegate { |
| handleAction(context: UI.Context.Context, actionId: string): boolean { |
| 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': |
| void 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.copy-styles': |
| void treeOutline.findTreeElement(node)?.copyStyles(); |
| return true; |
| case 'elements.undo': |
| void SDK.DOMModel.DOMModelUndoStack.instance().undo(); |
| ElementsPanel.instance().stylesWidget.forceUpdate(); |
| return true; |
| case 'elements.redo': |
| void SDK.DOMModel.DOMModelUndoStack.instance().redo(); |
| ElementsPanel.instance().stylesWidget.forceUpdate(); |
| return true; |
| case 'elements.show-styles': |
| ElementsPanel.instance().selectAndShowSidebarTab(SidebarPaneTabId.Styles); |
| return true; |
| case 'elements.show-computed': |
| ElementsPanel.instance().selectAndShowSidebarTab(SidebarPaneTabId.Computed); |
| return true; |
| case 'elements.toggle-eye-dropper': { |
| const colorSwatchPopoverIcon = UI.Context.Context.instance().flavor(ColorSwatchPopoverIcon); |
| if (!colorSwatchPopoverIcon) { |
| return false; |
| } |
| |
| void colorSwatchPopoverIcon.toggleEyeDropper(); |
| } |
| } |
| return false; |
| } |
| |
| static instance(opts: { |
| forceNew: boolean|null, |
| }|undefined = {forceNew: null}): ElementsActionDelegate { |
| const {forceNew} = opts; |
| if (!elementsActionDelegateInstance || forceNew) { |
| elementsActionDelegateInstance = new ElementsActionDelegate(); |
| } |
| |
| return elementsActionDelegateInstance; |
| } |
| } |
| |
| let pseudoStateMarkerDecoratorInstance: PseudoStateMarkerDecorator; |
| export class PseudoStateMarkerDecorator implements MarkerDecorator { |
| static instance(opts: { |
| forceNew: boolean|null, |
| } = {forceNew: null}): PseudoStateMarkerDecorator { |
| const {forceNew} = opts; |
| if (!pseudoStateMarkerDecoratorInstance || forceNew) { |
| pseudoStateMarkerDecoratorInstance = new PseudoStateMarkerDecorator(); |
| } |
| |
| return pseudoStateMarkerDecoratorInstance; |
| } |
| |
| decorate(node: SDK.DOMModel.DOMNode): { |
| title: string, |
| color: string, |
| }|null { |
| const pseudoState = node.domModel().cssModel().pseudoState(node); |
| if (!pseudoState) { |
| return null; |
| } |
| |
| return {color: 'orange', title: i18nString(UIStrings.elementStateS, {PH1: ':' + pseudoState.join(', :')})}; |
| } |
| } |