Skip to content

Commit 7c0bf4d

Browse files
bmeurerDevtools-frontend LUCI CQ
authored andcommitted
Adopt UI eng vision in the LayersWidget
This is still using the `<devtools-tree-outline>` and will need to be migrated further once we have the final tree outline component ready. Drive-by-fix: Fix the event handler memory leaks (currently these didn't bite us because we still use singletons, but once that'd be fixed, we'd otherwise leak every instance). Drive-by-fix: No need to use a shadow root for the LayersWidget itself, there are only two trivially styled elements inside anyways. Fixed: 407750478 Change-Id: Ia796b8bbe9abdacf1a0ae12e8ceff682cc067aca Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6822162 Commit-Queue: Benedikt Meurer <[email protected]> Auto-Submit: Benedikt Meurer <[email protected]> Reviewed-by: Philip Pfaffe <[email protected]>
1 parent bb5307b commit 7c0bf4d

File tree

2 files changed

+97
-76
lines changed

2 files changed

+97
-76
lines changed

front_end/panels/elements/LayersWidget.ts

Lines changed: 95 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
// Copyright (c) 2022 The Chromium Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4-
/* eslint-disable rulesdir/no-imperative-dom-api */
54

5+
import type * as Common from '../../core/common/common.js';
66
import * as i18n from '../../core/i18n/i18n.js';
77
import * as SDK from '../../core/sdk/sdk.js';
88
import type * as Protocol from '../../generated/protocol.js';
9+
import * as Lit from '../../third_party/lit/lit.js';
910
import * as TreeOutline from '../../ui/components/tree_outline/tree_outline.js';
1011
import * as UI from '../../ui/legacy/legacy.js';
1112
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
1213

1314
import {ElementsPanel} from './ElementsPanel.js';
1415
import layersWidgetStyles from './layersWidget.css.js';
1516

17+
const {render, html, Directives: {ref}} = Lit;
18+
1619
const UIStrings = {
1720
/**
1821
* @description Title of a section in the Element State Pane Widget of the Elements panel.
@@ -28,95 +31,122 @@ const UIStrings = {
2831
const str_ = i18n.i18n.registerUIStrings('panels/elements/LayersWidget.ts', UIStrings);
2932
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
3033

31-
let layersWidgetInstance: LayersWidget;
34+
interface ViewInput {
35+
rootLayer: Protocol.CSS.CSSLayerData;
36+
}
3237

33-
export class LayersWidget extends UI.Widget.Widget {
34-
private cssModel?: SDK.CSSModel.CSSModel|null;
35-
private layerTreeComponent = new TreeOutline.TreeOutline.TreeOutline<string>();
38+
interface ViewOutput {
39+
treeOutline: TreeOutline.TreeOutline.TreeOutline<string>|undefined;
40+
}
3641

37-
constructor() {
38-
super({
39-
jslog: `${VisualLogging.pane('css-layers')}`,
40-
useShadowDom: true,
41-
});
42-
this.registerRequiredCSS(layersWidgetStyles);
42+
type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void;
4343

44-
this.contentElement.className = 'styles-layers-pane';
45-
UI.UIUtils.createTextChild(this.contentElement.createChild('div'), i18nString(UIStrings.cssLayersTitle));
44+
const DEFAULT_VIEW: View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => {
45+
const makeTreeNode = (parentId: string) => (layer: Protocol.CSS.CSSLayerData) => {
46+
const subLayers = layer.subLayers;
47+
const name = SDK.CSSModel.CSSModel.readableLayerName(layer.name);
48+
const treeNodeData = layer.order + ': ' + name;
49+
const id = parentId ? parentId + '.' + name : name;
50+
if (!subLayers) {
51+
return {treeNodeData, id};
52+
}
53+
return {
54+
treeNodeData,
55+
id,
56+
children: async () => subLayers.sort((layer1, layer2) => layer1.order - layer2.order).map(makeTreeNode(id)),
57+
};
58+
};
59+
const {defaultRenderer} = TreeOutline.TreeOutline;
60+
const tree = [makeTreeNode('')(input.rootLayer)];
61+
const data: TreeOutline.TreeOutline.TreeOutlineData<string> = {
62+
defaultRenderer,
63+
tree,
64+
};
65+
const captureTreeOutline = (e?: Element): void => {
66+
output.treeOutline = e as typeof output.treeOutline;
67+
};
68+
const template = html`
69+
<style>${layersWidgetStyles}</style>
70+
<div class="layers-widget">
71+
<div class="layers-widget-title">${UIStrings.cssLayersTitle}</div>
72+
<devtools-tree-outline ${ref(captureTreeOutline)}
73+
.data=${data}></devtools-tree-outline>
74+
</div>
75+
`;
76+
render(template, target);
77+
};
4678

47-
this.contentElement.appendChild(this.layerTreeComponent);
79+
let layersWidgetInstance: LayersWidget;
4880

49-
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.update, this);
50-
}
81+
export class LayersWidget extends UI.Widget.Widget {
82+
#node: SDK.DOMModel.DOMNode|null = null;
83+
#view: View;
84+
#layerToReveal: string|null = null;
5185

52-
private updateModel(cssModel: SDK.CSSModel.CSSModel|null): void {
53-
if (this.cssModel === cssModel) {
54-
return;
55-
}
56-
if (this.cssModel) {
57-
this.cssModel.removeEventListener(SDK.CSSModel.Events.StyleSheetChanged, this.update, this);
58-
}
59-
this.cssModel = cssModel;
60-
if (this.cssModel) {
61-
this.cssModel.addEventListener(SDK.CSSModel.Events.StyleSheetChanged, this.update, this);
62-
}
86+
constructor(view: View = DEFAULT_VIEW) {
87+
super({jslog: `${VisualLogging.pane('css-layers')}`});
88+
this.#view = view;
6389
}
6490

65-
override wasShown(): Promise<void> {
91+
override wasShown(): void {
6692
super.wasShown();
67-
return this.update();
93+
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.#onDOMNodeChanged, this);
94+
this.#onDOMNodeChanged({data: UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode)});
6895
}
6996

70-
async update(): Promise<void> {
71-
if (!this.isShowing()) {
97+
override wasHidden(): void {
98+
UI.Context.Context.instance().addFlavorChangeListener(SDK.DOMModel.DOMNode, this.#onDOMNodeChanged, this);
99+
this.#onDOMNodeChanged({data: null});
100+
super.wasHidden();
101+
}
102+
103+
#onDOMNodeChanged(event: Common.EventTarget.EventTargetEvent<SDK.DOMModel.DOMNode|null>): void {
104+
const node = event.data?.enclosingElementOrSelf();
105+
if (this.#node === node) {
72106
return;
73107
}
74-
75-
let node = UI.Context.Context.instance().flavor(SDK.DOMModel.DOMNode);
76-
if (node) {
77-
node = node.enclosingElementOrSelf();
108+
if (this.#node) {
109+
this.#node.domModel().cssModel().removeEventListener(
110+
SDK.CSSModel.Events.StyleSheetChanged, this.requestUpdate, this);
78111
}
79-
if (!node) {
80-
// do something meaningful?
81-
return;
112+
this.#node = event.data;
113+
if (this.#node) {
114+
this.#node.domModel().cssModel().addEventListener(
115+
SDK.CSSModel.Events.StyleSheetChanged, this.requestUpdate, this);
116+
}
117+
if (this.isShowing()) {
118+
this.requestUpdate();
82119
}
120+
}
83121

84-
this.updateModel(node.domModel().cssModel());
85-
if (!this.cssModel) {
122+
override async performUpdate(): Promise<void> {
123+
if (!this.#node) {
86124
return;
87125
}
88-
const makeTreeNode = (parentId: string) => (layer: Protocol.CSS.CSSLayerData) => {
89-
const subLayers = layer.subLayers;
90-
const name = SDK.CSSModel.CSSModel.readableLayerName(layer.name);
91-
const treeNodeData = layer.order + ': ' + name;
92-
const id = parentId ? parentId + '.' + name : name;
93-
if (!subLayers) {
94-
return {treeNodeData, id};
95-
}
96-
return {
97-
treeNodeData,
98-
id,
99-
children: () =>
100-
Promise.resolve(subLayers.sort((layer1, layer2) => layer1.order - layer2.order).map(makeTreeNode(id))),
101-
};
102-
};
103-
const rootLayer = await this.cssModel.getRootLayer(node.id);
104-
this.layerTreeComponent.data = {
105-
defaultRenderer: TreeOutline.TreeOutline.defaultRenderer,
106-
tree: [makeTreeNode('')(rootLayer)],
107-
};
108126

109-
// We only expand the first 5 user-defined layers to not make the
110-
// view too overwhelming.
111-
await this.layerTreeComponent.expandRecursively(5);
127+
const rootLayer = await this.#node.domModel().cssModel().getRootLayer(this.#node.id);
128+
const input = {rootLayer};
129+
const output: ViewOutput = {treeOutline: undefined};
130+
this.#view(input, output, this.contentElement);
131+
132+
if (output.treeOutline) {
133+
// We only expand the first 5 user-defined layers to not make the
134+
// view too overwhelming.
135+
await output.treeOutline.expandRecursively(5);
136+
if (this.#layerToReveal) {
137+
await output.treeOutline.expandToAndSelectTreeNodeId(this.#layerToReveal);
138+
this.#layerToReveal = null;
139+
}
140+
}
112141
}
113142

114143
async revealLayer(layerName: string): Promise<void> {
115144
if (!this.isShowing()) {
116145
ElementsPanel.instance().showToolbarPane(this, ButtonProvider.instance().item());
117146
}
118-
await this.update();
119-
return await this.layerTreeComponent.expandToAndSelectTreeNodeId('implicit outer layer.' + layerName);
147+
this.#layerToReveal = `implicit outer layer.${layerName}`;
148+
this.requestUpdate();
149+
await this.updateComplete;
120150
}
121151

122152
static instance(opts: {

front_end/panels/elements/layersWidget.css

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* found in the LICENSE file.
55
*/
66

7-
.styles-layers-pane {
7+
.layers-widget {
88
overflow: hidden;
99
padding-left: 2px;
1010
background-color: var(--sys-color-cdt-base-container);
@@ -13,16 +13,7 @@
1313
padding-bottom: 2px;
1414
}
1515

16-
.styles-layers-pane > div {
16+
.layers-widget > .layers-widget-title {
1717
font-weight: bold;
1818
margin: 8px 4px 6px;
1919
}
20-
21-
.styles-layers-pane > table {
22-
width: 100%;
23-
border-spacing: 0;
24-
}
25-
26-
.styles-layers-pane td {
27-
padding: 0;
28-
}

0 commit comments

Comments
 (0)