UNPKG

chrome-devtools-frontend

Version:
248 lines (226 loc) • 9.9 kB
// Copyright (c) 2020 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. import * as Common from '../../core/common/common.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as ElementsComponents from './components/components.js'; import {ElementsPanel} from './ElementsPanel.js'; const nodeToLayoutElement = (node: SDK.DOMModel.DOMNode): ElementsComponents.LayoutPane.LayoutElement => { const className = node.getAttribute('class'); const nodeId = node.id; return { id: nodeId, color: '#000', name: node.localName(), domId: node.getAttribute('id'), domClasses: className ? className.split(/\s+/).filter(s => Boolean(s)) : undefined, enabled: false, reveal: (): void => { void ElementsPanel.instance().revealAndSelectNode(node, true, true); void node.scrollIntoView(); }, highlight: (): void => { node.highlight(); }, hideHighlight: (): void => { SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(); }, toggle: (_value: boolean): never => { throw new Error('Not implemented'); }, setColor(_value: string): never { throw new Error('Not implemented'); }, }; }; const gridNodesToElements = (nodes: SDK.DOMModel.DOMNode[]): ElementsComponents.LayoutPane.LayoutElement[] => { return nodes.map(node => { const layoutElement = nodeToLayoutElement(node); const nodeId = node.id; return { ...layoutElement, color: node.domModel().overlayModel().colorOfGridInPersistentOverlay(nodeId) || '#000', enabled: node.domModel().overlayModel().isHighlightedGridInPersistentOverlay(nodeId), toggle: (value: boolean): void => { if (value) { node.domModel().overlayModel().highlightGridInPersistentOverlay(nodeId); } else { node.domModel().overlayModel().hideGridInPersistentOverlay(nodeId); } }, setColor(value: string): void { this.color = value; node.domModel().overlayModel().setColorOfGridInPersistentOverlay(nodeId, value); }, }; }); }; const flexContainerNodesToElements = (nodes: SDK.DOMModel.DOMNode[]): ElementsComponents.LayoutPane.LayoutElement[] => { return nodes.map(node => { const layoutElement = nodeToLayoutElement(node); const nodeId = node.id; return { ...layoutElement, color: node.domModel().overlayModel().colorOfFlexInPersistentOverlay(nodeId) || '#000', enabled: node.domModel().overlayModel().isHighlightedFlexContainerInPersistentOverlay(nodeId), toggle: (value: boolean): void => { if (value) { node.domModel().overlayModel().highlightFlexContainerInPersistentOverlay(nodeId); } else { node.domModel().overlayModel().hideFlexContainerInPersistentOverlay(nodeId); } }, setColor(value: string): void { this.color = value; node.domModel().overlayModel().setColorOfFlexInPersistentOverlay(nodeId, value); }, }; }); }; let layoutSidebarPaneInstance: LayoutSidebarPane; export class LayoutSidebarPane extends UI.ThrottledWidget.ThrottledWidget { private layoutPane: ElementsComponents.LayoutPane.LayoutPane; private readonly settings: string[]; private readonly uaShadowDOMSetting: Common.Settings.Setting<boolean>; // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any private readonly boundOnSettingChanged: (event: any) => void; private domModels: SDK.DOMModel.DOMModel[]; constructor() { super(true /* isWebComponent */); this.layoutPane = new ElementsComponents.LayoutPane.LayoutPane(); this.contentElement.appendChild(this.layoutPane); this.settings = ['showGridLineLabels', 'showGridTrackSizes', 'showGridAreas', 'extendGridLines']; this.uaShadowDOMSetting = Common.Settings.Settings.instance().moduleSetting('showUAShadowDOM'); this.boundOnSettingChanged = this.onSettingChanged.bind(this); this.domModels = []; } static instance(opts: { forceNew: boolean|null, }|undefined = {forceNew: null}): LayoutSidebarPane { const {forceNew} = opts; if (!layoutSidebarPaneInstance || forceNew) { layoutSidebarPaneInstance = new LayoutSidebarPane(); } return layoutSidebarPaneInstance; } modelAdded(domModel: SDK.DOMModel.DOMModel): void { const overlayModel = domModel.overlayModel(); overlayModel.addEventListener(SDK.OverlayModel.Events.PersistentGridOverlayStateChanged, this.update, this); overlayModel.addEventListener( SDK.OverlayModel.Events.PersistentFlexContainerOverlayStateChanged, this.update, this); this.domModels.push(domModel); } modelRemoved(domModel: SDK.DOMModel.DOMModel): void { const overlayModel = domModel.overlayModel(); overlayModel.removeEventListener(SDK.OverlayModel.Events.PersistentGridOverlayStateChanged, this.update, this); overlayModel.removeEventListener( SDK.OverlayModel.Events.PersistentFlexContainerOverlayStateChanged, this.update, this); this.domModels = this.domModels.filter(model => model !== domModel); } private async fetchNodesByStyle(style: { name: string, value: string, }[]): Promise<SDK.DOMModel.DOMNode[]> { const showUAShadowDOM = this.uaShadowDOMSetting.get(); const nodes = []; for (const domModel of this.domModels) { try { const nodeIds = await domModel.getNodesByStyle(style, true /* pierce */); for (const nodeId of nodeIds) { const node = domModel.nodeForId(nodeId); if (node !== null && (showUAShadowDOM || !node.ancestorUserAgentShadowRoot())) { nodes.push(node); } } } catch (error) { // TODO(crbug.com/1167706): Sometimes in E2E tests the layout panel is updated after a DOM node // has been removed. This causes an error that a node has not been found. // We can skip nodes that resulted in an error. console.warn(error); } } return nodes; } private async fetchGridNodes(): Promise<SDK.DOMModel.DOMNode[]> { return await this.fetchNodesByStyle([{name: 'display', value: 'grid'}, {name: 'display', value: 'inline-grid'}]); } private async fetchFlexContainerNodes(): Promise<SDK.DOMModel.DOMNode[]> { return await this.fetchNodesByStyle([{name: 'display', value: 'flex'}, {name: 'display', value: 'inline-flex'}]); } private mapSettings(): ElementsComponents.LayoutPaneUtils.Setting[] { const settings = []; for (const settingName of this.settings) { const setting = Common.Settings.Settings.instance().moduleSetting(settingName); const settingValue = setting.get(); const settingType = setting.type(); if (!settingType) { throw new Error('A setting provided to LayoutSidebarPane does not have a setting type'); } if (settingType !== Common.Settings.SettingType.BOOLEAN && settingType !== Common.Settings.SettingType.ENUM) { throw new Error('A setting provided to LayoutSidebarPane does not have a supported setting type'); } const mappedSetting = { type: settingType, name: setting.name, title: setting.title(), }; if (typeof settingValue === 'boolean') { settings.push({ ...mappedSetting, value: settingValue, options: setting.options().map(opt => ({ ...opt, value: (opt.value as boolean), })), }); } else if (typeof settingValue === 'string') { settings.push({ ...mappedSetting, value: settingValue, options: setting.options().map(opt => ({ ...opt, value: (opt.value as string), })), }); } } return settings; } async doUpdate(): Promise<void> { this.layoutPane.data = { gridElements: gridNodesToElements(await this.fetchGridNodes()), flexContainerElements: flexContainerNodesToElements(await this.fetchFlexContainerNodes()), settings: this.mapSettings(), }; } // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // eslint-disable-next-line @typescript-eslint/no-explicit-any onSettingChanged(event: any): void { Common.Settings.Settings.instance().moduleSetting(event.data.setting).set(event.data.value); } wasShown(): void { for (const setting of this.settings) { Common.Settings.Settings.instance().moduleSetting(setting).addChangeListener(this.update, this); } this.layoutPane.addEventListener('settingchanged', this.boundOnSettingChanged); for (const domModel of this.domModels) { this.modelRemoved(domModel); } this.domModels = []; SDK.TargetManager.TargetManager.instance().observeModels(SDK.DOMModel.DOMModel, this); UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.update, this); this.uaShadowDOMSetting.addChangeListener(this.update, this); this.update(); } willHide(): void { for (const setting of this.settings) { Common.Settings.Settings.instance().moduleSetting(setting).removeChangeListener(this.update, this); } this.layoutPane.removeEventListener('settingchanged', this.boundOnSettingChanged); SDK.TargetManager.TargetManager.instance().unobserveModels(SDK.DOMModel.DOMModel, this); UI.Context.Context.instance().removeFlavorChangeListener(SDK.DOMModel.DOMNode, this.update, this); this.uaShadowDOMSetting.removeChangeListener(this.update, this); } }