// Copyright 2021 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 */ /* * Copyright (C) 2007 Apple Inc. All rights reserved. * Copyright (C) 2009 Joseph Pecoraro * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import '../../ui/legacy/legacy.js'; import * as Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as ComputedStyleModule from '../../models/computed_style/computed_style.js'; import * as TreeOutline from '../../ui/components/tree_outline/tree_outline.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 * as Lit from '../../ui/lit/lit.js'; import * as ElementsComponents from './components/components.js'; import computedStyleWidgetStyles from './computedStyleWidget.css.js'; import {ImagePreviewPopover} from './ImagePreviewPopover.js'; import {categorizePropertyName, type Category, DefaultCategoryOrder} from './PropertyNameCategories.js'; import {Renderer, rendererBase, type RenderingContext, StringRenderer, URLRenderer} from './PropertyRenderer.js'; import {StylePropertiesSection} from './StylePropertiesSection.js'; const {html, render} = Lit; const {bindToSetting} = UI.UIUtils; const UIStrings = { /** * @description Text for a checkbox setting that controls whether the user-supplied filter text * excludes all CSS propreties which are filtered out, or just greys them out. In Computed Style * Widget of the Elements panel */ showAll: 'Show all', /** * @description Text for a checkbox setting that controls whether similar CSS properties should be * grouped together or not. In Computed Style Widget of the Elements panel. */ group: 'Group', /** * [ * @description Text shown to the user when a filter is applied to the computed CSS properties, but * no properties matched the filter and thus no results were returned. */ noMatchingProperty: 'No matching property', /** * @description Context menu item in Elements panel to navigate to the source code location of the * CSS selector that was clicked on. */ navigateToSelectorSource: 'Navigate to selector source', /** * @description Context menu item in Elements panel to navigate to the corresponding CSS style rule * for this computed property. */ navigateToStyle: 'Navigate to styles', /** * @description Text announced to screen readers when a filter is applied to the computed styles list, informing them of the filter term and the number of results. * @example {example} PH1 * @example {5} PH2 */ filterUpdateAriaText: `Filter applied: {PH1}. Total Results: {PH2}`, } as const; const str_ = i18n.i18n.registerUIStrings('panels/elements/ComputedStyleWidget.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); function matchProperty(name: string, value: string): SDK.CSSPropertyParser.BottomUpTreeMatching|null { return SDK.CSSPropertyParser.matchDeclaration(name, value, [ new SDK.CSSPropertyParserMatchers.ColorMatcher(), new SDK.CSSPropertyParserMatchers.URLMatcher(), new SDK.CSSPropertyParserMatchers.StringMatcher() ]); } function renderPropertyContents( node: SDK.DOMModel.DOMNode, cache: Map, propertyName: string, propertyValue: string): {name: Element, value: Element} { const cacheKey = propertyName + ':' + propertyValue; const valueFromCache = cache.get(cacheKey); if (valueFromCache) { return valueFromCache; } const name = Renderer.renderNameElement(propertyName); name.slot = 'name'; const value = Renderer .renderValueElement( {name: propertyName, value: propertyValue}, matchProperty(propertyName, propertyValue), [new ColorRenderer(), new URLRenderer(null, node), new StringRenderer()]) .valueElement; value.slot = 'value'; cache.set(cacheKey, {name, value}); return {name, value}; } /** * Note: this function is called for each tree node on each render, so we need * to ensure nothing expensive runs here, or if it does it is safely cached. **/ const createPropertyElement = (node: SDK.DOMModel.DOMNode, cache: Map, propertyName: string, propertyValue: string, traceable: boolean, inherited: boolean, activeProperty: SDK.CSSProperty.CSSProperty|undefined, onContextMenu: ((event: Event) => void)): Lit.TemplateResult => { const {name, value} = renderPropertyContents(node, cache, propertyName, propertyValue); // clang-format off return html` { if (activeProperty) { navigateToSource(activeProperty, event); } }}> ${name} ${value} `; // clang-format on }; const createTraceElement = (node: SDK.DOMModel.DOMNode, property: SDK.CSSProperty.CSSProperty, isPropertyOverloaded: boolean, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, linkifier: Components.Linkifier.Linkifier): ElementsComponents.ComputedStyleTrace.ComputedStyleTrace => { const trace = new ElementsComponents.ComputedStyleTrace.ComputedStyleTrace(); const {valueElement} = Renderer.renderValueElement( property, matchProperty(property.name, property.value), [new ColorRenderer(), new URLRenderer(null, node), new StringRenderer()]); valueElement.slot = 'trace-value'; trace.appendChild(valueElement); const rule = (property.ownerStyle.parentRule as SDK.CSSRule.CSSStyleRule | null); let ruleOriginNode; if (rule) { ruleOriginNode = StylePropertiesSection.createRuleOriginNode(matchedStyles, linkifier, rule); } let selector = 'element.style'; if (rule) { selector = rule.selectorText(); } else if (property.ownerStyle.type === SDK.CSSStyleDeclaration.Type.Animation) { selector = property.ownerStyle.animationName() ? `${property.ownerStyle.animationName()} animation` : 'animation style'; } else if (property.ownerStyle.type === SDK.CSSStyleDeclaration.Type.Transition) { selector = 'transitions style'; } trace.data = { selector, active: !isPropertyOverloaded, onNavigateToSource: navigateToSource.bind(null, property), ruleOriginNode, }; return trace; }; // clang-format off class ColorRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.ColorMatch) { // clang-format on override render(match: SDK.CSSPropertyParserMatchers.ColorMatch, context: RenderingContext): Node[] { const color = Common.Color.parse(match.text); if (!color) { return [document.createTextNode(match.text)]; } const swatch = new InlineEditor.ColorSwatch.ColorSwatch(); swatch.setReadonly(true); swatch.renderColor(color); const valueElement = document.createElement('span'); valueElement.textContent = match.text; swatch.addEventListener( InlineEditor.ColorSwatch.ColorChangedEvent.eventName, (event: InlineEditor.ColorSwatch.ColorChangedEvent) => { const {data: {color}} = event; valueElement.textContent = color.getAuthoredText() ?? color.asString(); }); context.addControl('color', swatch); return [swatch, valueElement]; } matcher(): SDK.CSSPropertyParserMatchers.ColorMatcher { return new SDK.CSSPropertyParserMatchers.ColorMatcher(); } } const navigateToSource = (cssProperty: SDK.CSSProperty.CSSProperty, event?: Event): void => { if (!event) { return; } void Common.Revealer.reveal(cssProperty); event.consume(true); }; const propertySorter = (propA: string, propB: string): number => { if (propA.startsWith('--') !== propB.startsWith('--')) { return propA.startsWith('--') ? 1 : -1; } if (propA.startsWith('-webkit') !== propB.startsWith('-webkit')) { return propA.startsWith('-webkit') ? 1 : -1; } const canonicalA = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propA); const canonicalB = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propB); return Platform.StringUtilities.compare(canonicalA, canonicalB); }; type ComputedStyleData = { tag: 'property', propertyName: string, propertyValue: string, inherited: boolean, }|{ tag: 'traceElement', property: SDK.CSSProperty.CSSProperty, rule: SDK.CSSRule.CSSRule | null, }|{ tag: 'category', name: string, }; interface ComputedStyleWidgetInput { computedStylesTree: TreeOutline.TreeOutline.TreeOutline; hasMatches: boolean; showInheritedComputedStylePropertiesSetting: Common.Settings.Setting; groupComputedStylesSetting: Common.Settings.Setting; onFilterChanged: (event: CustomEvent) => void; filterText: string; onRegexToggled: () => void; includeToolbar: boolean; } type View = (input: ComputedStyleWidgetInput, output: null, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, _output, target) => { // clang-format off render(html` ${input.includeToolbar ? html`
${i18nString(UIStrings.showAll)} ${i18nString(UIStrings.group)}
` : Lit.nothing} ${input.computedStylesTree} ${!input.hasMatches ? html`
${i18nString(UIStrings.noMatchingProperty)}
` : ''} `, target); // clang-format on }; export class ComputedStyleWidget extends UI.Widget.VBox { #computedStyleModel?: ComputedStyleModule.ComputedStyleModel.ComputedStyleModel; #nodeStyle: ComputedStyleModule.ComputedStyleModel.ComputedStyle|null = null; #matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles|null = null; #propertyTraces: Map|null = null; private readonly showInheritedComputedStylePropertiesSetting: Common.Settings.Setting; private readonly groupComputedStylesSetting: Common.Settings.Setting; private filterRegex: RegExp|null = null; private readonly linkifier: Components.Linkifier.Linkifier; private readonly imagePreviewPopover: ImagePreviewPopover; /** * Rendering a property's name and value is expensive, and each time we do it * it generates a new HTML element. If we call this directly from our Lit * components, we will generate a brand new DOM element on each single render. * This is very expensive and unnecessary - for the majority of re-renders a * property's name and value does not change. So we cache the rest of rendering * the name and value in a map, where the key used is a combination of the * property's name and value. This ensures that we only re-generate this element * if the node itself changes. * The resulting Element nodes are inserted into the ComputedStyleProperty * component via s, ensuring that Lit doesn't directly render/re-render * the element. * We have to store this cache per widget because it is possible to have * multiple widgets for the same NodeId showing at once. In that case, if we * reuse the same element from the cache, only one of the widgets will be * populated, because an HTML node cannot be in two locations at once. */ #propertyElementsCache = new Map(); #computedStylesTree = new TreeOutline.TreeOutline.TreeOutline(); #treeData?: TreeOutline.TreeOutline.TreeOutlineData; readonly #view: View; /** * TODO(b/407751272): the state here is confusing (3 instance variables relating to filtering). * There is also a bug where the Toolbar Input's regex flag cannot be * controlled, so if you set a regex filter here, the toolbar might not * reflect it. */ #filterText = ''; #filterIsRegex = false; #allowUserControl = true; constructor(element?: HTMLElement, view = DEFAULT_VIEW) { super(element, {useShadowDom: true}); this.#view = view; this.contentElement.classList.add('styles-sidebar-computed-style-widget'); this.showInheritedComputedStylePropertiesSetting = Common.Settings.Settings.instance().createSetting('show-inherited-computed-style-properties', false); this.showInheritedComputedStylePropertiesSetting.addChangeListener(this.requestUpdate.bind(this)); this.groupComputedStylesSetting = Common.Settings.Settings.instance().createSetting('group-computed-styles', false); this.groupComputedStylesSetting.addChangeListener(() => { this.requestUpdate(); }); this.filterRegex = null; this.linkifier = new Components.Linkifier.Linkifier(maxLinkLength); this.imagePreviewPopover = new ImagePreviewPopover(this.contentElement, event => { const link = event.composedPath()[0]; if (link instanceof Element) { return link; } return null; }, () => this.#computedStyleModel ? this.#computedStyleModel.node : null); this.#updateView({hasMatches: true}); } override onResize(): void { const isNarrow = this.contentElement.offsetWidth < 260; this.#computedStylesTree.classList.toggle('computed-narrow', isNarrow); } get filterText(): RegExp|string { if (this.#filterIsRegex) { return new RegExp(this.#filterText); } return this.#filterText; } get filterIsRegex(): boolean { return this.#filterIsRegex; } set filterText(newFilter: RegExp|string) { if (typeof newFilter === 'string') { this.#filterText = newFilter; this.#filterIsRegex = false; } else { this.#filterText = newFilter.source; this.#filterIsRegex = true; } this.filterRegex = this.#buildFilterRegex(this.#filterText); this.requestUpdate(); } get allowUserControl(): boolean { return this.#allowUserControl; } set allowUserControl(inc: boolean) { this.#allowUserControl = inc; this.requestUpdate(); } /** * @param input.hasMatches Whether any properties matched the current filter (or if any properties exist at all). */ #updateView({hasMatches}: {hasMatches: boolean}): void { this.#view( { computedStylesTree: this.#computedStylesTree, includeToolbar: this.#allowUserControl, hasMatches, showInheritedComputedStylePropertiesSetting: this.showInheritedComputedStylePropertiesSetting, groupComputedStylesSetting: this.groupComputedStylesSetting, onFilterChanged: this.onFilterChanged.bind(this), filterText: this.#filterText, onRegexToggled: this.onRegexToggled.bind(this), }, null, this.contentElement); } get nodeStyle(): ComputedStyleModule.ComputedStyleModel.ComputedStyle|null { return this.#nodeStyle; } set nodeStyle(nodeStyle: ComputedStyleModule.ComputedStyleModel.ComputedStyle|null) { this.#nodeStyle = nodeStyle; this.requestUpdate(); } get matchedStyles(): SDK.CSSMatchedStyles.CSSMatchedStyles|null { return this.#matchedStyles; } set matchedStyles(matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles|null) { this.#matchedStyles = matchedStyles; this.requestUpdate(); } set propertyTraces(propertyTraces: Map|null) { this.#propertyTraces = propertyTraces; this.requestUpdate(); } get computedStyleModel(): ComputedStyleModule.ComputedStyleModel.ComputedStyleModel|undefined { return this.#computedStyleModel; } set computedStyleModel(computedStyleModel: ComputedStyleModule.ComputedStyleModel.ComputedStyleModel) { this.#computedStyleModel = computedStyleModel; this.requestUpdate(); } #shouldGroupStyles(): boolean { return this.#allowUserControl && this.groupComputedStylesSetting.get(); } #shouldShowAllStyles(): boolean { return this.#allowUserControl && this.showInheritedComputedStylePropertiesSetting.get(); } override async performUpdate(): Promise { const nodeStyles = this.#nodeStyle; const matchedStyles = this.#matchedStyles; if (!nodeStyles || !matchedStyles) { this.#updateView({hasMatches: false}); return; } if (this.#shouldGroupStyles()) { await this.rebuildGroupedList(nodeStyles, matchedStyles); } else { await this.rebuildAlphabeticalList(nodeStyles, matchedStyles); } } private async rebuildAlphabeticalList( nodeStyle: ComputedStyleModule.ComputedStyleModel.ComputedStyle, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles): Promise { this.imagePreviewPopover.hide(); this.linkifier.reset(); const cssModel = this.#computedStyleModel?.cssModel(); if (!cssModel) { return; } const uniqueProperties = [...nodeStyle.computedStyle.keys()]; uniqueProperties.sort(propertySorter); const node = nodeStyle.node; const propertyTraces = this.#propertyTraces || new Map(); const nonInheritedProperties = this.computeNonInheritedProperties(matchedStyles); const showInherited = this.#shouldShowAllStyles(); const tree: Array> = []; for (const propertyName of uniqueProperties) { const propertyValue = nodeStyle.computedStyle.get(propertyName) || ''; const canonicalName = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propertyName); const isInherited = !nonInheritedProperties.has(canonicalName); if (!showInherited && isInherited && !alwaysShownComputedProperties.has(propertyName)) { continue; } if (!showInherited && propertyName.startsWith('--')) { continue; } if (propertyName !== canonicalName && propertyValue === nodeStyle.computedStyle.get(canonicalName)) { continue; } tree.push(this.buildTreeNode(propertyTraces, propertyName, propertyValue, isInherited)); } const defaultRenderer = this.createTreeNodeRenderer(propertyTraces, node, matchedStyles); this.#treeData = { tree, compact: true, defaultRenderer, }; this.filterAlphabeticalList(); } private async rebuildGroupedList( nodeStyle: ComputedStyleModule.ComputedStyleModel.ComputedStyle|null, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles|null): Promise { this.imagePreviewPopover.hide(); this.linkifier.reset(); const cssModel = this.#computedStyleModel?.cssModel(); if (!nodeStyle || !matchedStyles || !cssModel) { this.#updateView({hasMatches: false}); return; } const node = nodeStyle.node; const propertyTraces = this.#propertyTraces || new Map(); const nonInheritedProperties = this.computeNonInheritedProperties(matchedStyles); const showInherited = this.showInheritedComputedStylePropertiesSetting.get(); const propertiesByCategory = new Map(); const tree: Array> = []; for (const [propertyName, propertyValue] of nodeStyle.computedStyle) { const canonicalName = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propertyName); const isInherited = !nonInheritedProperties.has(canonicalName); if (!showInherited && isInherited && !alwaysShownComputedProperties.has(propertyName)) { continue; } if (!showInherited && propertyName.startsWith('--')) { continue; } if (propertyName !== canonicalName && propertyValue === nodeStyle.computedStyle.get(canonicalName)) { continue; } const categories = categorizePropertyName(propertyName); for (const category of categories) { if (!propertiesByCategory.has(category)) { propertiesByCategory.set(category, []); } propertiesByCategory.get(category)?.push(propertyName); } } this.#computedStylesTree.removeChildren(); for (const category of DefaultCategoryOrder) { const properties = propertiesByCategory.get(category); if (properties && properties.length > 0) { const propertyNodes: Array> = []; for (const propertyName of properties) { const propertyValue = nodeStyle.computedStyle.get(propertyName) || ''; const canonicalName = SDK.CSSMetadata.cssMetadata().canonicalPropertyName(propertyName); const isInherited = !nonInheritedProperties.has(canonicalName); propertyNodes.push(this.buildTreeNode(propertyTraces, propertyName, propertyValue, isInherited)); } tree.push({id: category, treeNodeData: {tag: 'category', name: category}, children: async () => propertyNodes}); } } const defaultRenderer = this.createTreeNodeRenderer(propertyTraces, node, matchedStyles); this.#treeData = { tree, compact: true, defaultRenderer, }; return await this.filterGroupLists(); } private buildTraceNode(property: SDK.CSSProperty.CSSProperty): TreeOutline.TreeOutlineUtils.TreeNode { const rule = property.ownerStyle.parentRule; return { treeNodeData: { tag: 'traceElement', property, rule, }, id: (rule?.origin || '') + ': ' + property.ownerStyle.styleSheetId + (property.range || property.name), }; } private createTreeNodeRenderer( propertyTraces: Map, domNode: SDK.DOMModel.DOMNode, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, ): (node: TreeOutline.TreeOutlineUtils.TreeNode, state: {isExpanded: boolean}) => Lit.TemplateResult { return node => { const data = node.treeNodeData; if (data.tag === 'property') { const trace = propertyTraces.get(data.propertyName); const activeProperty = trace?.find( property => matchedStyles.propertyState(property) === SDK.CSSMatchedStyles.PropertyState.ACTIVE); const propertyElement = createPropertyElement( domNode, this.#propertyElementsCache, data.propertyName, data.propertyValue, propertyTraces.has(data.propertyName), data.inherited, activeProperty, event => { if (activeProperty) { this.handleContextMenuEvent(matchedStyles, activeProperty, event); } }); return propertyElement; } if (data.tag === 'traceElement') { const isPropertyOverloaded = matchedStyles.propertyState(data.property) === SDK.CSSMatchedStyles.PropertyState.OVERLOADED; const traceElement = createTraceElement(domNode, data.property, isPropertyOverloaded, matchedStyles, this.linkifier); traceElement.addEventListener( 'contextmenu', this.handleContextMenuEvent.bind(this, matchedStyles, data.property)); return html`${traceElement}`; } return html`${data.name}`; }; } private buildTreeNode( propertyTraces: Map, propertyName: string, propertyValue: string, isInherited: boolean): TreeOutline.TreeOutlineUtils.TreeNode { const treeNodeData: ComputedStyleData = { tag: 'property', propertyName, propertyValue, inherited: isInherited, }; const trace = propertyTraces.get(propertyName); const jslogContext = propertyName.startsWith('--') ? 'custom-property' : propertyName; if (!trace) { return { treeNodeData, jslogContext, id: propertyName, }; } return { treeNodeData, jslogContext, id: propertyName, children: async () => trace.map(this.buildTraceNode), }; } private handleContextMenuEvent( matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, property: SDK.CSSProperty.CSSProperty, event: Event): void { const contextMenu = new UI.ContextMenu.ContextMenu(event); const rule = property.ownerStyle.parentRule; if (rule) { const header = rule.header; if (header && !header.isAnonymousInlineStyleSheet()) { contextMenu.defaultSection().appendItem(i18nString(UIStrings.navigateToSelectorSource), () => { StylePropertiesSection.tryNavigateToRuleLocation(matchedStyles, rule); }, {jslogContext: 'navigate-to-selector-source'}); } } contextMenu.defaultSection().appendItem( i18nString(UIStrings.navigateToStyle), () => Common.Revealer.reveal(property), {jslogContext: 'navigate-to-style'}); void contextMenu.show(); } private computeNonInheritedProperties(matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles): Set { const result = new Set(); for (const style of matchedStyles.nodeStyles()) { for (const property of style.allProperties()) { if (!matchedStyles.propertyState(property)) { continue; } result.add(SDK.CSSMetadata.cssMetadata().canonicalPropertyName(property.name)); } } return result; } #buildFilterRegex(text: string): RegExp|null { if (!text) { return null; } if (this.#filterIsRegex) { try { return new RegExp(text, 'i'); } catch { // Invalid regex: fall through to plain-text matching. } } return new RegExp(Platform.StringUtilities.escapeForRegExp(text), 'i'); } private async onRegexToggled(): Promise { this.#filterIsRegex = !this.#filterIsRegex; await this.filterComputedStyles(this.#buildFilterRegex(this.#filterText)); } private async onFilterChanged(event: CustomEvent): Promise { this.#filterText = event.detail; await this.filterComputedStyles(this.#buildFilterRegex(event.detail)); if (event.detail && this.#computedStylesTree.data && this.#computedStylesTree.data.tree) { UI.ARIAUtils.LiveAnnouncer.alert(i18nString( UIStrings.filterUpdateAriaText, {PH1: event.detail, PH2: this.#computedStylesTree.data.tree.length})); } } async filterComputedStyles(regex: RegExp|null): Promise { this.filterRegex = regex; if (this.groupComputedStylesSetting.get()) { return await this.filterGroupLists(); } return this.filterAlphabeticalList(); } private nodeFilter(node: TreeOutline.TreeOutlineUtils.TreeNode): boolean { const regex = this.filterRegex; const data = node.treeNodeData; if (data.tag === 'property') { const matched = !regex || regex.test(data.propertyName) || regex.test(data.propertyValue); return matched; } return true; } private filterAlphabeticalList(): void { if (!this.#treeData) { return; } const tree = this.#treeData.tree.filter(this.nodeFilter.bind(this)); this.#computedStylesTree.data = { tree, defaultRenderer: this.#treeData.defaultRenderer, compact: this.#treeData.compact, }; this.#updateView({hasMatches: Boolean(tree.length)}); } private async filterGroupLists(): Promise { if (!this.#treeData) { return; } const tree: Array> = []; for (const group of this.#treeData.tree) { const data = group.treeNodeData; if (data.tag !== 'category' || !group.children) { continue; } const properties = await group.children(); const filteredChildren = properties.filter(this.nodeFilter.bind(this)); if (filteredChildren.length) { tree.push( {id: data.name, treeNodeData: {tag: 'category', name: data.name}, children: async () => filteredChildren}); } } this.#computedStylesTree.data = { tree, defaultRenderer: this.#treeData.defaultRenderer, compact: this.#treeData.compact, }; await this.#computedStylesTree.expandRecursively(0); this.#updateView({hasMatches: Boolean(tree.length)}); } } const maxLinkLength = 30; const alwaysShownComputedProperties = new Set(['display', 'height', 'width']);