chrome-devtools-frontend
Version:
Chrome DevTools UI
196 lines (169 loc) • 6.74 kB
text/typescript
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Lit from '../../third_party/lit/lit.js';
import * as TreeOutline from '../../ui/components/tree_outline/tree_outline.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import {ElementsPanel} from './ElementsPanel.js';
import layersWidgetStyles from './layersWidget.css.js';
const {render, html, Directives: {ref}} = Lit;
const UIStrings = {
/**
* @description Title of a section in the Element State Pane Widget of the Elements panel.
* The widget shows the layers present in the context of the currently selected node.
* */
cssLayersTitle: 'CSS layers',
/**
* @description Tooltip text in Element State Pane Widget of the Elements panel.
* For a button that opens a tool that shows the layers present in the current document.
*/
toggleCSSLayers: 'Toggle CSS Layers view',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/elements/LayersWidget.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
interface ViewInput {
rootLayer: Protocol.CSS.CSSLayerData;
}
interface ViewOutput {
treeOutline: TreeOutline.TreeOutline.TreeOutline<string>|undefined;
}
type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
const DEFAULT_VIEW: View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => {
const makeTreeNode = (parentId: string) => (layer: Protocol.CSS.CSSLayerData) => {
const subLayers = layer.subLayers;
const name = SDK.CSSModel.CSSModel.readableLayerName(layer.name);
const treeNodeData = layer.order + ': ' + name;
const id = parentId ? parentId + '.' + name : name;
if (!subLayers) {
return {treeNodeData, id};
}
return {
treeNodeData,
id,
children: async () => subLayers.sort((layer1, layer2) => layer1.order - layer2.order).map(makeTreeNode(id)),
};
};
const {defaultRenderer} = TreeOutline.TreeOutline;
const tree = [makeTreeNode('')(input.rootLayer)];
const data: TreeOutline.TreeOutline.TreeOutlineData<string> = {
defaultRenderer,
tree,
};
const captureTreeOutline = (e?: Element): void => {
output.treeOutline = e as typeof output.treeOutline;
};
const template = html`
<style>${layersWidgetStyles}</style>
<div class="layers-widget">
<div class="layers-widget-title">${UIStrings.cssLayersTitle}</div>
<devtools-tree-outline ${ref(captureTreeOutline)}
.data=${data}></devtools-tree-outline>
</div>
`;
render(template, target);
};
let layersWidgetInstance: LayersWidget;
export class LayersWidget extends UI.Widget.Widget {
#node: SDK.DOMModel.DOMNode|null = null;
#view: View;
#layerToReveal: string|null = null;
constructor(view: View = DEFAULT_VIEW) {
super({jslog: `${VisualLogging.pane('css-layers')}`});
this.#view = view;
}
override wasShown(): void {
super.wasShown();
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.#onDOMNodeChanged, this);
this.#onDOMNodeChanged({data: UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode)});
}
override wasHidden(): void {
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.#onDOMNodeChanged, this);
this.#onDOMNodeChanged({data: null});
super.wasHidden();
}
#onDOMNodeChanged(event: Common.EventTarget.EventTargetEvent<SDK.DOMModel.DOMNode|null>): void {
const node = event.data?.enclosingElementOrSelf();
if (this.#node === node) {
return;
}
if (this.#node) {
this.#node.domModel().cssModel().removeEventListener(
SDK.CSSModel.Events.StyleSheetChanged, this.requestUpdate, this);
}
this.#node = event.data;
if (this.#node) {
this.#node.domModel().cssModel().addEventListener(
SDK.CSSModel.Events.StyleSheetChanged, this.requestUpdate, this);
}
if (this.isShowing()) {
this.requestUpdate();
}
}
override async performUpdate(): Promise<void> {
if (!this.#node) {
return;
}
const rootLayer = await this.#node.domModel().cssModel().getRootLayer(this.#node.id);
const input = {rootLayer};
const output: ViewOutput = {treeOutline: undefined};
this.#view(input, output, this.contentElement);
if (output.treeOutline) {
// We only expand the first 5 user-defined layers to not make the
// view too overwhelming.
await output.treeOutline.expandRecursively(5);
if (this.#layerToReveal) {
await output.treeOutline.expandToAndSelectTreeNodeId(this.#layerToReveal);
this.#layerToReveal = null;
}
}
}
async revealLayer(layerName: string): Promise<void> {
if (!this.isShowing()) {
ElementsPanel.instance().showToolbarPane(this, ButtonProvider.instance().item());
}
this.#layerToReveal = `implicit outer layer.${layerName}`;
this.requestUpdate();
await this.updateComplete;
}
static instance(opts: {
forceNew: boolean|null,
}|undefined = {forceNew: null}): LayersWidget {
const {forceNew} = opts;
if (!layersWidgetInstance || forceNew) {
layersWidgetInstance = new LayersWidget();
}
return layersWidgetInstance;
}
}
let buttonProviderInstance: ButtonProvider;
export class ButtonProvider implements UI.Toolbar.Provider {
private readonly button: UI.Toolbar.ToolbarToggle;
private constructor() {
this.button = new UI.Toolbar.ToolbarToggle(i18nString(UIStrings.toggleCSSLayers), 'layers', 'layers-filled');
this.button.setVisible(false);
this.button.addEventListener(UI.Toolbar.ToolbarButton.Events.CLICK, this.clicked, this);
this.button.element.classList.add('monospace');
this.button.element.setAttribute('jslog', `${VisualLogging.toggleSubpane('css-layers').track({click: true})}`);
}
static instance(opts: {
forceNew: boolean|null,
} = {forceNew: null}): ButtonProvider {
const {forceNew} = opts;
if (!buttonProviderInstance || forceNew) {
buttonProviderInstance = new ButtonProvider();
}
return buttonProviderInstance;
}
private clicked(): void {
const view = LayersWidget.instance();
ElementsPanel.instance().showToolbarPane(!view.isShowing() ? view : null, this.button);
}
item(): UI.Toolbar.ToolbarToggle {
return this.button;
}
}