chrome-devtools-frontend
Version:
Chrome DevTools UI
328 lines (269 loc) • 10.5 kB
text/typescript
// Copyright 2026 The Chromium Authors
// 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 type * as SDK from '../../core/sdk/sdk.js';
import * as ComputedStyle from '../../models/computed_style/computed_style.js';
import type * as TextUtils from '../../models/text_utils/text_utils.js';
import * as InlineEditor from '../../ui/legacy/components/inline_editor/inline_editor.js';
import * as Components from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import * as ElementsComponents from './components/components.js';
import {StylePropertiesSection} from './StylePropertiesSection.js';
import type {StylePropertyTreeElement} from './StylePropertyTreeElement.js';
import type {StylesContainer} from './StylesContainer.js';
import stylesSidebarPaneStyles from './stylesSidebarPane.css.js';
import {WebCustomData} from './WebCustomData.js';
interface ViewInput {
sections: StylePropertiesSection[];
}
type View = (input: ViewInput, output_: undefined, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, _output, target) => {
render(
html`
<style>${stylesSidebarPaneStyles}</style>
<div class="style-panes-wrapper" jslog=${VisualLogging.section('standalone-styles').track({
resize: true
})}>
<div class="styles-pane">
${input.sections.map(section => section.element)}
</div>
</div>
`,
target);
};
export const enum Events {
STYLES_UPDATE_COMPLETED = 'StylesUpdateCompleted',
}
export interface EventTypes {
[Events.STYLES_UPDATE_COMPLETED]: void;
}
export class StandaloneStylesContainer extends Common.ObjectWrapper.eventMixin<EventTypes, typeof UI.Widget.VBox>(
UI.Widget.VBox) implements StylesContainer {
activeCSSAngle: InlineEditor.CSSAngle.CSSAngle|null = null;
isEditingStyle = false;
readonly sectionByElement = new WeakMap<Node, StylePropertiesSection>();
// TODO: Reference the MAX_LINK_LENGTH from StylesSidebarPane at a later stage, when we have a reference to it.
readonly linkifier: Components.Linkifier.Linkifier =
new Components.Linkifier.Linkifier(23, /* useLinkDecorator */ true);
#webCustomData: WebCustomData|undefined;
userOperation = false;
#sections: StylePropertiesSection[] = [];
#swatchPopoverHelper = new InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper();
#computedStyleModelInternal = new ComputedStyle.ComputedStyleModel.ComputedStyleModel();
#view: View;
#filter: RegExp|null = null;
#rebuildThrottler = new Common.Throttler.Throttler(200);
constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
super(element, {useShadowDom: true});
this.#view = view;
this.#computedStyleModelInternal.addEventListener(
ComputedStyle.ComputedStyleModel.Events.CSS_MODEL_CHANGED, this.#onCSSModelChanged, this);
this.#computedStyleModelInternal.addEventListener(
ComputedStyle.ComputedStyleModel.Events.COMPUTED_STYLE_CHANGED, this.#onComputedStyleChanged, this);
}
#onComputedStyleChanged(): void {
if (this.isEditingStyle || this.userOperation) {
return;
}
this.#rebuildAndUpdate();
}
#rebuildAndUpdate(): void {
void this.#rebuildThrottler.schedule(async () => {
this.node()?.domModel().cssModel().discardCachedMatchedCascade();
await this.#updateSections();
this.requestUpdate();
});
}
async #onCSSModelChanged(
event: Common.EventTarget.EventTargetEvent<ComputedStyle.ComputedStyleModel.CSSModelChangedEvent>):
Promise<void> {
// We only recreate sections if this update is more than an "edit" operation.
// Sections will pull their own updates in the case of an "edit".
if (event?.data && 'edit' in event.data && event?.data.edit) {
return;
}
if (this.isEditingStyle || this.userOperation) {
return;
}
this.#rebuildAndUpdate();
}
get webCustomData(): WebCustomData|undefined {
if (!this.#webCustomData &&
Common.Settings.Settings.instance().moduleSetting('show-css-property-documentation-on-hover').get()) {
this.#webCustomData = WebCustomData.create();
}
return this.#webCustomData;
}
async #updateSections(signal?: AbortSignal): Promise<void> {
const node = this.node();
if (!node) {
this.#sections = [];
return;
}
const cssModel = node.domModel().cssModel();
const matchedStyles = await cssModel.cachedMatchedCascadeForNode(node);
if (signal?.aborted) {
return;
}
const parentNodeId = matchedStyles?.getParentLayoutNodeId();
const [parentStyles, computedStyles, extraStyles] = await Promise.all([
parentNodeId ? cssModel.getComputedStyle(parentNodeId) : null, cssModel.getComputedStyle(node.id),
cssModel.getComputedStyleExtraFields(node.id)
]);
if (signal?.aborted) {
return;
}
if (!matchedStyles) {
return;
}
const newSections: StylePropertiesSection[] = [];
let sectionIdx = 0;
for (const style of matchedStyles.nodeStyles()) {
const section = new StylePropertiesSection(
this, matchedStyles, style, sectionIdx++, computedStyles, parentStyles, extraStyles);
section.update(true);
newSections.push(section);
this.sectionByElement.set(section.element, section);
}
this.#sections = newSections;
this.#updateFilter();
this.swatchPopoverHelper().reposition();
}
override async performUpdate(signal?: AbortSignal): Promise<void> {
await this.#updateSections(signal);
signal?.throwIfAborted();
const viewInput: ViewInput = {
sections: this.#sections.filter(section => !section.isHidden()),
};
this.#view(viewInput, undefined, this.contentElement);
this.#onUpdateFinished();
}
#onUpdateFinished(): void {
this.dispatchEventToListeners(Events.STYLES_UPDATE_COMPLETED);
}
#updateFilter(): void {
for (const section of this.#sections) {
section.updateFilter();
}
}
swatchPopoverHelper(): InlineEditor.SwatchPopoverHelper.SwatchPopoverHelper {
return this.#swatchPopoverHelper;
}
// TODO: Refactor StylesContainer to use getter for node(), so that we can have a `node` setter here: set node().
set domNode(node: SDK.DOMModel.DOMNode|null) {
if (this.#computedStyleModelInternal.node === node) {
return;
}
this.#computedStyleModelInternal.node = node;
this.forceUpdate();
}
set filter(regex: RegExp|null) {
this.#filter = regex;
this.#updateFilter();
this.requestUpdate();
}
node(): SDK.DOMModel.DOMNode|null {
return this.#computedStyleModelInternal.node;
}
cssModel(): SDK.CSSModel.CSSModel|null {
return this.#computedStyleModelInternal.cssModel();
}
computedStyleModel(): ComputedStyle.ComputedStyleModel.ComputedStyleModel {
return this.#computedStyleModelInternal;
}
setActiveProperty(_treeElement: StylePropertyTreeElement|null): void {
}
refreshUpdate(editedSection: StylePropertiesSection, editedTreeElement?: StylePropertyTreeElement): void {
if (editedTreeElement) {
for (const section of this.#sections) {
section.updateVarFunctions(editedTreeElement);
}
}
if (this.isEditingStyle) {
this.#onUpdateFinished();
return;
}
for (const section of this.#sections) {
section.update(section === editedSection);
}
this.swatchPopoverHelper().reposition();
this.#onUpdateFinished();
}
filterRegex(): RegExp|null {
return this.#filter;
}
setEditingStyle(editing: boolean): void {
this.isEditingStyle = editing;
}
setUserOperation(userOperation: boolean): void {
this.userOperation = userOperation;
}
forceUpdate(): void {
this.hideAllPopovers();
this.#rebuildAndUpdate();
}
hideAllPopovers(): void {
this.#swatchPopoverHelper.hide();
if (this.activeCSSAngle) {
this.activeCSSAngle.minify();
this.activeCSSAngle = null;
}
}
allSections(): StylePropertiesSection[] {
return this.#sections;
}
getVariablePopoverContents(
matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, variableName: string,
computedValue: string|null): ElementsComponents.CSSVariableValueView.CSSVariableValueView {
const registration = matchedStyles.getRegisteredProperty(variableName);
return new ElementsComponents.CSSVariableValueView.CSSVariableValueView({
variableName,
value: computedValue ?? undefined,
// TODO: provide a goToDefinition to jump to the StylesSidebarPane
details: registration ? {registration, goToDefinition: () => {}} : undefined,
});
}
getVariableParserError(_matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, _variableName: string):
ElementsComponents.CSSVariableValueView.CSSVariableParserError|null {
return null;
}
jumpToFunctionDefinition(_functionName: string): void {
}
continueEditingElement(_sectionIndex: number, _propertyIndex: number): void {
}
revealProperty(_cssProperty: SDK.CSSProperty.CSSProperty): void {
}
resetFocus(): void {
const firstVisibleSection = this.#sections[0]?.findCurrentOrNextVisible(true);
if (firstVisibleSection) {
firstVisibleSection.element.tabIndex = this.hasFocus() ? -1 : 0;
}
}
removeSection(_section: StylePropertiesSection): void {
}
focusedSectionIndex(): number {
return this.#sections.findIndex(section => section.element.hasFocus());
}
addBlankSection(
_insertAfterSection: StylePropertiesSection, _styleSheetHeader: SDK.CSSStyleSheetHeader.CSSStyleSheetHeader,
_ruleLocation: TextUtils.TextRange.TextRange): void {
}
jumpToProperty(_propertyName: string, _sectionName?: string, _blockName?: string): boolean {
return false;
}
jumpToSectionBlock(_section: string): void {
}
jumpToFontPaletteDefinition(_paletteName: string): void {
}
jumpToDeclaration(_valueSource: SDK.CSSMatchedStyles.CSSValueSource): void {
}
addStyleUpdateListener(listener: () => void): void {
this.addEventListener(Events.STYLES_UPDATE_COMPLETED, listener);
}
removeStyleUpdateListener(listener: () => void): void {
this.removeEventListener(Events.STYLES_UPDATE_COMPLETED, listener);
}
}