UNPKG

chrome-devtools-frontend

Version:
328 lines (269 loc) • 10.5 kB
// 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); } }