Skip to content

Commit 42ac720

Browse files
danilsomsikovDevtools-frontend LUCI CQ
authored andcommitted
Adopt UI eng vision in the front_end/panels/elements/DOMLinkifier.ts
Bug: 407751668 Change-Id: I0240ac372049d0a7b081dde49a0edddc671ad019 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6519591 Auto-Submit: Danil Somsikov <dsv@chromium.org> Reviewed-by: Benedikt Meurer <bmeurer@chromium.org> Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
1 parent dd96ef2 commit 42ac720

File tree

1 file changed

+148
-113
lines changed

1 file changed

+148
-113
lines changed

front_end/panels/elements/DOMLinkifier.ts

Lines changed: 148 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
// Copyright 2018 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

65
import * as Common from '../../core/common/common.js';
76
import * as i18n from '../../core/i18n/i18n.js';
87
import * as SDK from '../../core/sdk/sdk.js';
98
import * as UI from '../../ui/legacy/legacy.js';
9+
import {Directives, html, nothing, render} from '../../ui/lit/lit.js';
1010
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
1111

1212
import domLinkifierStyles from './domLinkifier.css.js';
1313

14+
const {classMap} = Directives;
15+
1416
const UIStrings = {
1517
/**
1618
* @description Text displayed when trying to create a link to a node in the UI, but the node
@@ -27,163 +29,196 @@ export interface Options extends Common.Linkifier.Options {
2729
disabled?: boolean;
2830
}
2931

30-
export const decorateNodeLabel = function(
31-
node: SDK.DOMModel.DOMNode, parentElement: HTMLElement, options: Options): void {
32-
const originalNode = node;
33-
const isPseudo = node.nodeType() === Node.ELEMENT_NODE && node.pseudoType();
34-
if (isPseudo && node.parentNode) {
35-
node = node.parentNode;
36-
}
37-
38-
// Special case rendering the node links for view transition pseudo elements.
39-
// We don't include the ancestor name in the node link because
40-
// they always have the same ancestor. See crbug.com/340633630.
41-
if (node.isViewTransitionPseudoNode()) {
42-
const pseudoElement = parentElement.createChild('span', 'extra node-label-pseudo');
43-
const viewTransitionPseudoText = `::${originalNode.pseudoType()}(${originalNode.pseudoIdentifier()})`;
44-
UI.UIUtils.createTextChild(pseudoElement, viewTransitionPseudoText);
45-
UI.Tooltip.Tooltip.install(parentElement, options.tooltip || viewTransitionPseudoText);
46-
return;
47-
}
48-
49-
const nameElement = parentElement.createChild('span', 'node-label-name');
50-
if (options.textContent) {
51-
nameElement.textContent = options.textContent;
52-
UI.Tooltip.Tooltip.install(parentElement, options.tooltip || options.textContent);
53-
return;
54-
}
55-
56-
let title = node.nodeNameInCorrectCase();
57-
nameElement.textContent = title;
58-
59-
const idAttribute = node.getAttribute('id');
60-
if (idAttribute) {
61-
const idElement = parentElement.createChild('span', 'node-label-id');
62-
const part = '#' + idAttribute;
63-
title += part;
64-
UI.UIUtils.createTextChild(idElement, part);
65-
66-
// Mark the name as extra, since the ID is more important.
67-
nameElement.classList.add('extra');
68-
}
69-
70-
const classAttribute = node.getAttribute('class');
71-
if (classAttribute) {
72-
const classes = classAttribute.split(/\s+/);
73-
if (classes.length) {
74-
const foundClasses = new Set<string>();
75-
const classesElement = parentElement.createChild('span', 'extra node-label-class');
76-
for (let i = 0; i < classes.length; ++i) {
77-
const className = classes[i];
78-
if (className && !options.hiddenClassList?.includes(className) && !foundClasses.has(className)) {
79-
const part = '.' + className;
80-
title += part;
81-
UI.UIUtils.createTextChild(classesElement, part);
82-
foundClasses.add(className);
83-
}
84-
}
85-
}
86-
}
87-
88-
if (isPseudo) {
89-
const pseudoIdentifier = originalNode.pseudoIdentifier();
90-
const pseudoElement = parentElement.createChild('span', 'extra node-label-pseudo');
91-
let pseudoText = '::' + originalNode.pseudoType();
92-
if (pseudoIdentifier) {
93-
pseudoText += `(${pseudoIdentifier})`;
94-
}
32+
interface ViewInput {
33+
dynamic?: boolean;
34+
disabled?: boolean;
35+
preventKeyboardFocus?: boolean;
36+
tagName?: string;
37+
id?: string;
38+
classes: string[];
39+
pseudo?: string;
40+
onClick: () => void;
41+
onMouseOver: () => void;
42+
onMouseLeave: () => void;
43+
}
9544

96-
UI.UIUtils.createTextChild(pseudoElement, pseudoText);
97-
title += pseudoText;
98-
}
99-
UI.Tooltip.Tooltip.install(parentElement, options.tooltip || title);
45+
export type View = (input: ViewInput, output: object, target: HTMLElement) => void;
46+
47+
const DEFAULT_VIEW: View = (input, _output, target: HTMLElement) => {
48+
// clang-format off
49+
render(html`${(input.tagName || input.pseudo) ?
50+
html`<style>${domLinkifierStyles}</style
51+
><span class="monospace"
52+
><button class="node-link text-button link-style ${classMap({
53+
'dynamic-link': Boolean(input.dynamic),
54+
disabled: Boolean(input.disabled)
55+
})}"
56+
jslog=${VisualLogging.link('node').track({click: true, keydown: 'Enter'})}
57+
tabindex=${input.preventKeyboardFocus ? -1 : 0}
58+
@click=${input.onClick}
59+
@mouseover=${input.onMouseOver}
60+
@mouseleave=${input.onMouseLeave}
61+
title=${[
62+
input.tagName ?? '',
63+
input.id ? `#${input.id}` : '',
64+
...input.classes.map(c => `.${c}`),
65+
input.pseudo ? `::${input.pseudo}` : '',
66+
].join(' ')}>${
67+
[
68+
input.tagName ? html`<span class="node-label-name">${input.tagName}</span>` : nothing,
69+
input.id ? html`<span class="node-label-id">#${input.id}</span>` : nothing,
70+
...input.classes.map(className => html`<span class="extra node-label-class">.${className}</span>`),
71+
input.pseudo ? html`<span class="extra node-label-pseudo">${input.pseudo}</span>` : nothing,
72+
]
73+
}</button
74+
></span>` : i18nString(UIStrings.node)}`, target, {host: input});
75+
// clang-format on
10076
};
10177

10278
export class DOMNodeLink extends UI.Widget.Widget {
10379
#node: SDK.DOMModel.DOMNode|undefined = undefined;
10480
#options: Options|undefined = undefined;
81+
#view: View;
10582

106-
constructor(element?: HTMLElement, node?: SDK.DOMModel.DOMNode, options?: Options) {
83+
constructor(element?: HTMLElement, node?: SDK.DOMModel.DOMNode, options?: Options, view = DEFAULT_VIEW) {
10784
super(true, undefined, element);
10885
this.element.classList.remove('vbox');
10986
this.#node = node;
11087
this.#options = options;
88+
this.#view = view;
11189
this.performUpdate();
11290
}
11391

11492
override performUpdate(): void {
115-
const node = this.#node;
11693
const options = this.#options ?? {
11794
tooltip: undefined,
11895
preventKeyboardFocus: undefined,
11996
textContent: undefined,
12097
isDynamicLink: false,
12198
disabled: false,
12299
};
123-
this.contentElement.removeChildren();
124-
if (!node) {
125-
this.contentElement.appendChild(document.createTextNode(i18nString(UIStrings.node)));
100+
const viewInput: ViewInput = {
101+
dynamic: options.isDynamicLink,
102+
disabled: options.disabled,
103+
preventKeyboardFocus: options.preventKeyboardFocus,
104+
classes: [],
105+
onClick: () => {
106+
void Common.Revealer.reveal(this.#node);
107+
return false;
108+
},
109+
onMouseOver: () => {
110+
this.#node?.highlight?.();
111+
},
112+
onMouseLeave: () => {
113+
SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
114+
},
115+
};
116+
if (!this.#node) {
117+
this.#view(viewInput, {}, this.contentElement);
126118
return;
127119
}
128120

129-
const root = this.contentElement.createChild('span', 'monospace');
130-
this.registerRequiredCSS(domLinkifierStyles);
131-
const link = root.createChild('button', 'node-link text-button link-style');
132-
link.classList.toggle('dynamic-link', options.isDynamicLink);
133-
link.classList.toggle('disabled', options.disabled);
134-
link.setAttribute('jslog', `${VisualLogging.link('node').track({click: true, keydown: 'Enter'})}`);
121+
let node = this.#node;
122+
const isPseudo = node.nodeType() === Node.ELEMENT_NODE && node.pseudoType();
123+
if (isPseudo && node.parentNode) {
124+
node = node.parentNode;
125+
}
135126

136-
decorateNodeLabel(node, link, options);
127+
// Special case rendering the node links for view transition pseudo elements.
128+
// We don't include the ancestor name in the node link because
129+
// they always have the same ancestor. See crbug.com/340633630.
130+
if (node.isViewTransitionPseudoNode()) {
131+
viewInput.pseudo = `::${this.#node.pseudoType()}(${this.#node.pseudoIdentifier()})`;
132+
this.#view(viewInput, {}, this.contentElement);
133+
return;
134+
}
135+
136+
if (options.textContent) {
137+
viewInput.tagName = options.textContent;
138+
this.#view(viewInput, {}, this.contentElement);
139+
return;
140+
}
137141

138-
link.addEventListener('click', () => {
139-
void Common.Revealer.reveal(node, false);
140-
return false;
141-
}, false);
142-
link.addEventListener('mouseover', node.highlight.bind(node, undefined), false);
143-
link.addEventListener('mouseleave', () => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight(), false);
142+
viewInput.tagName = node.nodeNameInCorrectCase();
144143

145-
if (options.preventKeyboardFocus) {
146-
link.tabIndex = -1;
144+
const idAttribute = node.getAttribute('id');
145+
if (idAttribute) {
146+
viewInput.id = idAttribute;
147147
}
148+
149+
const classAttribute = node.getAttribute('class');
150+
if (classAttribute) {
151+
const classes = classAttribute.split(/\s+/);
152+
if (classes.length) {
153+
const foundClasses = new Set<string>();
154+
for (let i = 0; i < classes.length; ++i) {
155+
const className = classes[i];
156+
if (className && !options.hiddenClassList?.includes(className) && !foundClasses.has(className)) {
157+
foundClasses.add(className);
158+
}
159+
}
160+
viewInput.classes = [...foundClasses];
161+
}
162+
}
163+
if (isPseudo) {
164+
const pseudoIdentifier = this.#node.pseudoIdentifier();
165+
let pseudoText = '::' + this.#node.pseudoType();
166+
if (pseudoIdentifier) {
167+
pseudoText += `(${pseudoIdentifier})`;
168+
}
169+
viewInput.pseudo = pseudoText;
170+
}
171+
this.#view(viewInput, {}, this.contentElement);
148172
}
149173
}
150174

175+
interface DeferredViewInput {
176+
preventKeyboardFocus?: boolean;
177+
onClick: () => void;
178+
}
179+
180+
type DeferredView = (input: DeferredViewInput, output: object, target: HTMLElement) => void;
181+
182+
const DEFERRED_DEFAULT_VIEW: DeferredView = (input, _output, target: HTMLElement) => {
183+
// clang-format off
184+
render(html`
185+
<style>${domLinkifierStyles}</style>
186+
<button class="node-link text-button link-style"
187+
jslog=${VisualLogging.link('node').track({click: true})}
188+
tabindex=${input.preventKeyboardFocus ? -1 : 0}
189+
@click=${input.onClick}
190+
@mousedown=${(e: Event) => e.consume()}>
191+
<slot></slot>
192+
</button>`, target, {host: input});
193+
// clang-format on
194+
};
195+
151196
export class DeferredDOMNodeLink extends UI.Widget.Widget {
152197
#deferredNode: SDK.DOMModel.DeferredDOMNode|undefined = undefined;
153198
#options: Options|undefined = undefined;
199+
#view: DeferredView;
154200

155-
constructor(element?: HTMLElement, deferredNode?: SDK.DOMModel.DeferredDOMNode, options?: Options) {
201+
constructor(
202+
element?: HTMLElement, deferredNode?: SDK.DOMModel.DeferredDOMNode, options?: Options,
203+
view: DeferredView = DEFERRED_DEFAULT_VIEW) {
156204
super(true, undefined, element);
157205
this.element.classList.remove('vbox');
158206
this.#deferredNode = deferredNode;
159207
this.#options = options;
208+
this.#view = view;
160209
this.performUpdate();
161210
}
162211

163212
override performUpdate(): void {
164-
this.contentElement.removeChildren();
165-
const deferredNode = this.#deferredNode;
166-
if (!deferredNode) {
167-
return;
168-
}
169-
const options = this.#options ?? {
170-
tooltip: undefined,
171-
preventKeyboardFocus: undefined,
213+
const viewInput = {
214+
preventKeyboardFocus: this.#options?.preventKeyboardFocus,
215+
onClick: () => {
216+
this.#deferredNode?.resolve?.(node => {
217+
void Common.Revealer.reveal(node);
218+
});
219+
},
172220
};
173-
this.registerRequiredCSS(domLinkifierStyles);
174-
const link = this.contentElement.createChild('button', 'node-link text-button link-style');
175-
link.setAttribute('jslog', `${VisualLogging.link('node').track({click: true})}`);
176-
link.createChild('slot');
177-
link.addEventListener('click', deferredNode.resolve.bind(deferredNode, onDeferredNodeResolved), false);
178-
link.addEventListener('mousedown', e => e.consume(), false);
179-
180-
if (options.preventKeyboardFocus) {
181-
link.tabIndex = -1;
182-
}
183-
184-
function onDeferredNodeResolved(node: SDK.DOMModel.DOMNode|null): void {
185-
void Common.Revealer.reveal(node);
186-
}
221+
this.#view(viewInput, {}, this.contentElement);
187222
}
188223
}
189224

0 commit comments

Comments
 (0)