chrome-devtools-frontend
Version:
Chrome DevTools UI
329 lines (290 loc) • 11.1 kB
text/typescript
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */
import type * as Common from '../../core/common/common.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as TextUtils from '../../core/text_utils/text_utils.js';
import * as CodeHighlighter from '../../ui/components/code_highlighter/code_highlighter.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Lit from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import {PanelUtils} from '../utils/utils.js';
import type {EditorHandles} from './ElementsTreeElement.js';
const {Directives: {ref}, html, render} = Lit;
export class AdoptedStyleSheetSetTreeElement extends UI.TreeOutline.TreeElement {
constructor(readonly adoptedStyleSheets: SDK.DOMModel.AdoptedStyleSheet[]) {
super('');
const documentElement = this.listItemElement.createChild('span');
UI.UIUtils.createTextChild(documentElement, '#adopted-style-sheets');
for (const adoptedStyleSheet of adoptedStyleSheets) {
this.appendChild(new AdoptedStyleSheetTreeElement(adoptedStyleSheet));
}
}
}
export class AdoptedStyleSheetTreeElement extends UI.TreeOutline.TreeElement {
private eventListener: Common.EventTarget.EventDescriptor|null = null;
constructor(readonly adoptedStyleSheet: SDK.DOMModel.AdoptedStyleSheet) {
super('');
const header = adoptedStyleSheet.cssModel.styleSheetHeaderForId(adoptedStyleSheet.id);
if (header) {
AdoptedStyleSheetTreeElement.createContents(header, this);
} else {
this.eventListener = adoptedStyleSheet.cssModel.addEventListener(
SDK.CSSModel.Events.StyleSheetAdded, this.onStyleSheetAdded, this);
}
}
onStyleSheetAdded({data: header}: Common.EventTarget.EventTargetEvent<SDK.CSSStyleSheetHeader.CSSStyleSheetHeader>):
void {
if (header.id === this.adoptedStyleSheet.id) {
AdoptedStyleSheetTreeElement.createContents(header, this);
this.adoptedStyleSheet.cssModel.removeEventListener(
SDK.CSSModel.Events.StyleSheetAdded, this.onStyleSheetAdded, this);
this.eventListener = null;
}
}
static createContents(header: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader, treeElement: UI.TreeOutline.TreeElement):
void {
const documentElement = treeElement.listItemElement.createChild('span');
const linkText = header.sourceURL;
UI.UIUtils.createTextChild(documentElement, '#adopted-style-sheet' + (linkText ? ' (' : ''));
if (linkText) {
documentElement.appendChild(Components.Linkifier.Linkifier.linkifyURL(linkText, {
text: linkText,
preventClick: true,
showColumnNumber: false,
}));
UI.UIUtils.createTextChild(documentElement, ')');
}
treeElement.appendChild(new AdoptedStyleSheetContentsTreeElement(header));
}
highlight(): void {
PanelUtils.highlightElement(this.listItemElement);
}
}
export class AdoptedStyleSheetContentsTreeElement extends UI.TreeOutline.TreeElement {
readonly widget: AdoptedStyleSheetContentsWidget;
private readonly widgetWrapper: HTMLElement;
constructor(styleSheetHeader: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader) {
super('');
this.widgetWrapper = document.createElement('div');
this.widgetWrapper.style.display = 'contents';
this.title = this.widgetWrapper;
this.widget = new AdoptedStyleSheetContentsWidget();
this.widget.styleSheetHeader = styleSheetHeader;
this.widget.element.classList.remove('vbox', 'flex-auto');
this.widget.element.style.display = 'contents';
this.widget.markAsRoot();
this.widget.show(this.widgetWrapper);
}
override onbind(): void {
if (!this.widget.isShowing()) {
this.widget.show(this.widgetWrapper);
}
}
override onunbind(): void {
this.widget.detach();
}
override async onpopulate(): Promise<void> {
if (!this.widget.text) {
await this.widget.fetchContent();
}
this.widget.requestUpdate();
await this.widget.updateComplete;
}
override ondblclick(event: Event): boolean {
if (this.widget.isEditing()) {
return false;
}
this.widget.startEditing(event.target as Element);
return false;
}
override onenter(): boolean {
if (this.widget.isEditing()) {
return false;
}
void this.widget.startEditing();
return true;
}
isEditing(): boolean {
return this.widget.isEditing();
}
}
export interface AdoptedStyleSheetContentsViewInput {
text: string;
isEditing: boolean;
onDblClick: (event: MouseEvent) => void;
}
export type AdoptedStyleSheetContentsView =
(input: AdoptedStyleSheetContentsViewInput, output: undefined, target: HTMLElement) => void;
export const DEFAULT_ADOPTED_STYLESHEET_CONTENTS_VIEW: AdoptedStyleSheetContentsView =
(input: AdoptedStyleSheetContentsViewInput, _output: undefined, target: HTMLElement): void => {
const highlightNode = ref(el => {
if (el) {
el.textContent = input.text;
void CodeHighlighter.CodeHighlighter.highlightNode(el, 'text/css');
}
});
// clang-format off
render(html`
<span class="webkit-html-text-node webkit-html-css-node"
jslog=${VisualLogging.value('css-text-node').track({
change: true,
dblclick: true,
})}
${highlightNode}
=${input.onDblClick}></span>
`, target);
// clang-format on
};
export class AdoptedStyleSheetContentsWidget extends UI.Widget.Widget {
#styleSheetHeader?: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader;
#text = '';
#fetchContentPromise: Promise<void>|null = null;
#editing: EditorHandles|null = null;
#view: AdoptedStyleSheetContentsView;
constructor(element?: HTMLElement, view: AdoptedStyleSheetContentsView = DEFAULT_ADOPTED_STYLESHEET_CONTENTS_VIEW) {
super(element, {useShadowDom: false});
this.#view = view;
}
get text(): string {
return this.#text;
}
set styleSheetHeader(header: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader|undefined) {
if (this.#styleSheetHeader === header) {
return;
}
if (this.#styleSheetHeader) {
this.#styleSheetHeader.cssModel().removeEventListener(SDK.CSSModel.Events.StyleSheetChanged,
this.#onStyleSheetChanged, this);
}
this.#styleSheetHeader = header;
this.#text = '';
if (this.isShowing() && this.#styleSheetHeader) {
this.#styleSheetHeader.cssModel().addEventListener(SDK.CSSModel.Events.StyleSheetChanged,
this.#onStyleSheetChanged, this);
void this.fetchContent();
}
}
get styleSheetHeader(): SDK.CSSStyleSheetHeader.CSSStyleSheetHeader|undefined {
return this.#styleSheetHeader;
}
override wasShown(): void {
super.wasShown();
if (this.#styleSheetHeader) {
this.#styleSheetHeader.cssModel().addEventListener(SDK.CSSModel.Events.StyleSheetChanged,
this.#onStyleSheetChanged, this);
if (!this.#text) {
void this.fetchContent();
}
}
}
override willHide(): void {
super.willHide();
if (this.#editing) {
this.#editing.cancel();
this.#editing = null;
}
if (this.#styleSheetHeader) {
this.#styleSheetHeader.cssModel().removeEventListener(SDK.CSSModel.Events.StyleSheetChanged,
this.#onStyleSheetChanged, this);
}
}
async fetchContent(): Promise<void> {
const header = this.#styleSheetHeader;
if (!header) {
return;
}
if (this.#fetchContentPromise) {
return await this.#fetchContentPromise;
}
this.#fetchContentPromise = (async () => {
try {
const data = await header.requestContentData();
if (!TextUtils.ContentData.ContentData.isError(data) && data.isTextContent) {
this.#text = data.text.replace(/^[\n\r]+|\s+$/g, '');
this.requestUpdate();
}
} finally {
this.#fetchContentPromise = null;
}
})();
return await this.#fetchContentPromise;
}
#onStyleSheetChanged({data: {styleSheetId}}:
Common.EventTarget.EventTargetEvent<SDK.CSSModel.StyleSheetChangedEvent>): void {
if (styleSheetId === this.#styleSheetHeader?.id) {
void this.fetchContent();
}
}
startEditing(target?: Element): void {
if (this.#editing || !this.#styleSheetHeader) {
return;
}
const textNode = (target ? (target as HTMLElement).enclosingNodeOrSelfWithClass('webkit-html-text-node') : null) ??
this.contentElement.querySelector('.webkit-html-text-node');
if (!textNode || UI.UIUtils.isBeingEdited(textNode)) {
return;
}
const dummyEditorHandles: EditorHandles = {
commit: () => {},
cancel: () => {},
resize: () => {},
};
this.#editing = dummyEditorHandles;
void (async () => {
if (!this.#styleSheetHeader) {
this.#editing = null;
return;
}
const data = await this.#styleSheetHeader.requestContentData();
textNode.textContent = (TextUtils.ContentData.ContentData.isError(data) || !data.isTextContent) ? '' : data.text;
const config = new UI.InplaceEditor.Config(
(element, newText, oldText) => this.#editingCommitted(element, newText, oldText),
() => this.#editingCancelled(),
undefined,
);
const editorHandles = UI.InplaceEditor.InplaceEditor.startEditing(textNode, config);
if (!editorHandles) {
this.#editing = null;
return;
}
this.#editing = {
commit: editorHandles.commit,
cancel: editorHandles.cancel,
resize: () => {},
};
const selection = this.contentElement.getComponentSelection();
selection?.selectAllChildren(textNode);
})();
}
async #editingCommitted(_element: Element, newText: string, oldText: string|null): Promise<void> {
this.#editing = null;
if (newText !== oldText && this.#styleSheetHeader) {
await this.#styleSheetHeader.cssModel().setStyleSheetText(this.#styleSheetHeader.id, newText, false);
}
this.#editingCancelled();
}
#editingCancelled(): void {
this.#editing = null;
void this.fetchContent();
}
isEditing(): boolean {
return this.#editing !== null;
}
override performUpdate(): void {
if (this.isEditing()) {
return;
}
this.#view({
text: this.#text,
isEditing: this.isEditing(),
onDblClick: (event: MouseEvent) => {
event.stopPropagation();
this.startEditing(event.target as Element);
},
},
undefined, this.contentElement);
}
}