UNPKG

chrome-devtools-frontend

Version:
1,175 lines (1,068 loc) • 139 kB
// Copyright 2018 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 */ /* eslint-disable @devtools/no-lit-render-outside-of-view */ import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Badges from '../../models/badges/badges.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import type * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js'; import * as Tooltips from '../../ui/components/tooltips/tooltips.js'; import {createIcon, Icon} from '../../ui/kit/kit.js'; import * as ColorPicker from '../../ui/legacy/components/color_picker/color_picker.js'; import * as InlineEditor from '../../ui/legacy/components/inline_editor/inline_editor.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 { BezierPopoverIcon, ColorSwatchPopoverIcon, ColorSwatchPopoverIconEvents, ShadowEvents, ShadowSwatchPopoverHelper, } from './ColorSwatchPopoverIcon.js'; import * as ElementsComponents from './components/components.js'; import {cssRuleValidatorsMap} from './CSSRuleValidator.js'; import {CSSValueTraceView} from './CSSValueTraceView.js'; import {ElementsPanel} from './ElementsPanel.js'; import { BinOpRenderer, type MatchRenderer, Renderer, rendererBase, RenderingContext, StringRenderer, type TracingContext, URLRenderer } from './PropertyRenderer.js'; import {StyleEditorWidget} from './StyleEditorWidget.js'; import type {StylePropertiesSection} from './StylePropertiesSection.js'; import {getCssDeclarationAsJavascriptProperty} from './StylePropertyUtils.js'; import { CSSPropertyPrompt, REGISTERED_PROPERTY_SECTION_NAME, StylesSidebarPane, } from './StylesSidebarPane.js'; const {html, nothing, render, Directives: {classMap}} = Lit; const ASTUtils = SDK.CSSPropertyParser.ASTUtils; const FlexboxEditor = ElementsComponents.StylePropertyEditor.FlexboxEditor; const GridEditor = ElementsComponents.StylePropertyEditor.GridEditor; const GridLanesEditor = ElementsComponents.StylePropertyEditor.GridLanesEditor; const UIStrings = { /** * @description Text in Color Swatch Popover Icon of the Elements panel */ shiftClickToChangeColorFormat: 'Shift + Click to change color format.', /** * @description Swatch icon element title in Color Swatch Popover Icon of the Elements panel * @example {Shift + Click to change color format.} PH1 */ openColorPickerS: 'Open color picker. {PH1}', /** * @description Context menu item for style property in edit mode */ togglePropertyAndContinueEditing: 'Toggle property and continue editing', /** * @description Context menu item for style property in edit mode */ openInSourcesPanel: 'Open in Sources panel', /** * @description A context menu item in Styles panel to copy CSS declaration */ copyDeclaration: 'Copy declaration', /** * @description A context menu item in Styles panel to copy CSS property */ copyProperty: 'Copy property', /** * @description A context menu item in the Watch Expressions Sidebar Pane of the Sources panel and Network pane request. */ copyValue: 'Copy value', /** * @description A context menu item in Styles panel to copy CSS rule */ copyRule: 'Copy rule', /** * @description A context menu item in Styles panel to copy all CSS declarations */ copyAllDeclarations: 'Copy all declarations', /** * @description A context menu item in Styles panel to view the computed CSS property value. */ viewComputedValue: 'View computed value', /** * @description Title of the button that opens the flexbox editor in the Styles panel. */ flexboxEditorButton: 'Open `flexbox` editor', /** * @description Title of the button that opens the CSS Grid editor in the Styles panel. */ gridEditorButton: 'Open `grid` editor', /** * @description Title of the button that opens the CSS Grid Lanes editor in the Styles panel. */ gridLanesEditorButton: 'Open `grid-lanes` editor', /** * @description A context menu item in Styles panel to copy CSS declaration as JavaScript property. */ copyCssDeclarationAsJs: 'Copy declaration as JS', /** * @description A context menu item in Styles panel to copy all declarations of CSS rule as JavaScript properties. */ copyAllCssDeclarationsAsJs: 'Copy all declarations as JS', /** * @description Title of the link in Styles panel to jump to the Animations panel. */ jumpToAnimationsPanel: 'Jump to Animations panel', /** * @description Text displayed in a tooltip shown when hovering over a CSS property value references a name that's not * defined and can't be linked to. * @example {--my-linkable-name} PH1 */ sIsNotDefined: '{PH1} is not defined', /** * @description Text in Styles Sidebar Pane of the Elements panel */ invalidPropertyValue: 'Invalid property value', /** * @description Text in Styles Sidebar Pane of the Elements panel */ unknownPropertyName: 'Unknown property name', /** * @description Announcement string for invalid properties. * @example {Invalid property value} PH1 * @example {font-size} PH2 * @example {invalidValue} PH3 */ invalidString: '{PH1}, property name: {PH2}, property value: {PH3}', /** * @description Title in the styles tab for the icon button for jumping to the anchor node. */ jumpToAnchorNode: 'Jump to anchor node', } as const; const str_ = i18n.i18n.registerUIStrings('panels/elements/StylePropertyTreeElement.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const parentMap = new WeakMap<StylesSidebarPane, StylePropertyTreeElement>(); interface StylePropertyTreeElementParams { stylesPane: StylesSidebarPane; section: StylePropertiesSection; matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles; property: SDK.CSSProperty.CSSProperty; isShorthand: boolean; inherited: boolean; overloaded: boolean; newProperty: boolean; } // clang-format off export class EnvFunctionRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.EnvFunctionMatch) { // clang-format on constructor( readonly treeElement: StylePropertyTreeElement|null, readonly matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, readonly computedStyles: Map<string, string>) { super(); } override render(match: SDK.CSSPropertyParserMatchers.EnvFunctionMatch, context: RenderingContext): Node[] { const [, fallbackNodes] = ASTUtils.callArgs(match.node); if (match.value) { const substitution = context.tracing?.substitution(); if (substitution) { if (match.varNameIsValid) { return [document.createTextNode(match.value)]; } return Renderer.render(fallbackNodes, substitution.renderingContext(context)).nodes; } } const span = document.createElement('span'); const func = this.treeElement?.getTracingTooltip('env', match.node, this.matchedStyles, this.computedStyles, context) ?? 'env'; const valueClass = classMap({'inactive-value': !match.varNameIsValid}); const fallbackClass = classMap({'inactive-value': match.varNameIsValid}); render( html`${func}(<span class=${valueClass}>${match.varName}</span>${ fallbackNodes ? html`, <span class=${fallbackClass}>${Renderer.render(fallbackNodes, context).nodes}</span>` : nothing})`, span, {host: span}); return [span]; } } // clang-format off export class FlexGridRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.FlexGridGridLanesMatch) { // clang-format on readonly #treeElement: StylePropertyTreeElement|null; readonly #stylesPane: StylesSidebarPane; constructor(stylesPane: StylesSidebarPane, treeElement: StylePropertyTreeElement|null) { super(); this.#treeElement = treeElement; this.#stylesPane = stylesPane; } override render(match: SDK.CSSPropertyParserMatchers.FlexGridGridLanesMatch, context: RenderingContext): Node[] { const children = Renderer.render(ASTUtils.siblings(ASTUtils.declValue(match.node)), context).nodes; if (!this.#treeElement?.editable()) { return children; } const key = `${this.#treeElement.section().getSectionIdx()}_${this.#treeElement.section().nextEditorTriggerButtonIdx}`; function getEditorClass(layoutType: SDK.CSSPropertyParserMatchers.LayoutType): typeof FlexboxEditor| typeof GridEditor|typeof GridLanesEditor { switch (layoutType) { case SDK.CSSPropertyParserMatchers.LayoutType.FLEX: return FlexboxEditor; case SDK.CSSPropertyParserMatchers.LayoutType.GRID: return GridEditor; case SDK.CSSPropertyParserMatchers.LayoutType.GRID_LANES: return GridLanesEditor; } } function getButtonTitle(layoutType: SDK.CSSPropertyParserMatchers.LayoutType): string { switch (layoutType) { case SDK.CSSPropertyParserMatchers.LayoutType.FLEX: return i18nString(UIStrings.flexboxEditorButton); case SDK.CSSPropertyParserMatchers.LayoutType.GRID: return i18nString(UIStrings.gridEditorButton); case SDK.CSSPropertyParserMatchers.LayoutType.GRID_LANES: return i18nString(UIStrings.gridLanesEditorButton); } } function getSwatchType(layoutType: SDK.CSSPropertyParserMatchers.LayoutType): Host.UserMetrics.SwatchType { switch (layoutType) { case SDK.CSSPropertyParserMatchers.LayoutType.FLEX: return Host.UserMetrics.SwatchType.FLEX; case SDK.CSSPropertyParserMatchers.LayoutType.GRID: return Host.UserMetrics.SwatchType.GRID; case SDK.CSSPropertyParserMatchers.LayoutType.GRID_LANES: return Host.UserMetrics.SwatchType.GRID_LANES; } } const button = StyleEditorWidget.createTriggerButton( this.#stylesPane, this.#treeElement.section(), getEditorClass(match.layoutType), getButtonTitle(match.layoutType), key); button.tabIndex = -1; button.setAttribute('jslog', `${VisualLogging.showStyleEditor().track({click: true}).context(match.layoutType)}`); this.#treeElement.section().nextEditorTriggerButtonIdx++; button.addEventListener('click', () => { Host.userMetrics.swatchActivated(getSwatchType(match.layoutType)); }); const helper = this.#stylesPane.swatchPopoverHelper(); if (helper.isShowing(StyleEditorWidget.instance()) && StyleEditorWidget.instance().getTriggerKey() === key) { helper.setAnchorElement(button); } return [...children, button]; } } // clang-format off export class CSSWideKeywordRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.CSSWideKeywordMatch) { // clang-format on readonly #treeElement: StylePropertyTreeElement|null; readonly #stylesPane: StylesSidebarPane; constructor(stylesPane: StylesSidebarPane, treeElement: StylePropertyTreeElement|null) { super(); this.#treeElement = treeElement; this.#stylesPane = stylesPane; } override render(match: SDK.CSSPropertyParserMatchers.CSSWideKeywordMatch, context: RenderingContext): Node[] { const resolvedProperty = match.resolveProperty(); if (!resolvedProperty) { return [document.createTextNode(match.text)]; } const swatch = new InlineEditor.LinkSwatch.LinkSwatch(); swatch.data = { text: match.text, tooltip: resolvedProperty ? undefined : {title: i18nString(UIStrings.sIsNotDefined, {PH1: match.text})}, isDefined: Boolean(resolvedProperty), onLinkActivate: () => resolvedProperty && this.#stylesPane.jumpToDeclaration(resolvedProperty), jslogContext: 'css-wide-keyword-link', }; if (SDK.CSSMetadata.cssMetadata().isColorAwareProperty(resolvedProperty.name) || SDK.CSSMetadata.cssMetadata().isCustomProperty(resolvedProperty.name)) { const color = Common.Color.parse(context.matchedResult.getComputedText(match.node)); if (color) { return [new ColorRenderer(this.#stylesPane, this.#treeElement).renderColorSwatch(color, swatch), swatch]; } } return [swatch]; } } // clang-format off export class VariableRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.VariableMatch) { // clang-format on readonly #stylesPane: StylesSidebarPane; readonly #treeElement: StylePropertyTreeElement|null; readonly #matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles; readonly #computedStyles: Map<string, string>; constructor( stylesPane: StylesSidebarPane, treeElement: StylePropertyTreeElement|null, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, computedStyles: Map<string, string>) { super(); this.#treeElement = treeElement; this.#stylesPane = stylesPane; this.#matchedStyles = matchedStyles; this.#computedStyles = computedStyles; } override render(match: SDK.CSSPropertyParserMatchers.VariableMatch, context: RenderingContext): Node[] { if (this.#treeElement?.property.ownerStyle.parentRule instanceof SDK.CSSRule.CSSFunctionRule) { return Renderer.render(ASTUtils.children(match.node), context).nodes; } const {declaration, value: variableValue} = match.resolveVariable() ?? {}; const fromFallback = variableValue === undefined; const computedValue = variableValue ?? match.fallbackValue(); const onLinkActivate = (name: string): void => this.#handleVarDefinitionActivate(declaration ?? name); const varSwatch = document.createElement('span'); const substitution = context.tracing?.substitution({match, context}); if (substitution) { if (declaration?.declaration) { const {nodes, cssControls} = Renderer.renderValueNodes( {name: declaration.name, value: declaration.value ?? ''}, substitution.cachedParsedValue(declaration.declaration, this.#matchedStyles, this.#computedStyles), getPropertyRenderers( declaration.name, declaration.style, this.#stylesPane, this.#matchedStyles, null, this.#computedStyles), substitution); cssControls.forEach((value, key) => value.forEach(control => context.addControl(key, control))); return nodes; } if (!declaration && match.fallback) { return Renderer.render(match.fallback, substitution.renderingContext(context)).nodes; } } const renderedFallback = match.fallback ? Renderer.render(match.fallback, context) : undefined; const varCall = this.#treeElement?.getTracingTooltip('var', match.node, this.#matchedStyles, this.#computedStyles, context); const tooltipContents = this.#stylesPane.getVariablePopoverContents(this.#matchedStyles, match.name, variableValue ?? null); const tooltipId = this.#treeElement?.getTooltipId('custom-property-var'); const tooltip = tooltipId ? {tooltipId} : undefined; // clang-format off render(html` <span data-title=${computedValue || ''} jslog=${VisualLogging.link('css-variable').track({click: true, hover: true})}> ${varCall ?? 'var'}( <devtools-link-swatch class=css-var-link .data=${{ tooltip, text: match.name, isDefined: computedValue !== null && !fromFallback, onLinkActivate, }}> </devtools-link-swatch> ${renderedFallback ? html`, ${renderedFallback.nodes}` : nothing}) </span> ${tooltipId ? html` <devtools-tooltip id=${tooltipId} variant=rich jslogContext=elements.css-var > ${tooltipContents} </devtools-tooltip> ` : ''} `, varSwatch); // clang-format on const color = computedValue && Common.Color.parse(computedValue); if (!color) { return [varSwatch]; } const colorSwatch = new ColorRenderer(this.#stylesPane, this.#treeElement).renderColorSwatch(color, varSwatch); context.addControl('color', colorSwatch); if (fromFallback) { renderedFallback?.cssControls.get('color')?.forEach( innerSwatch => innerSwatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, ev => { colorSwatch.color = ev.data.color; })); } return [colorSwatch, varSwatch]; } #handleVarDefinitionActivate(variable: string|SDK.CSSMatchedStyles.CSSValueSource): void { Host.userMetrics.actionTaken(Host.UserMetrics.Action.CustomPropertyLinkClicked); Host.userMetrics.swatchActivated(Host.UserMetrics.SwatchType.VAR_LINK); if (typeof variable === 'string') { this.#stylesPane.jumpToProperty(variable) || this.#stylesPane.jumpToProperty('initial-value', variable, REGISTERED_PROPERTY_SECTION_NAME); } else if (variable.declaration instanceof SDK.CSSProperty.CSSProperty) { this.#stylesPane.revealProperty(variable.declaration); } else if (variable.declaration instanceof SDK.CSSMatchedStyles.CSSRegisteredProperty) { this.#stylesPane.jumpToProperty('initial-value', variable.name, REGISTERED_PROPERTY_SECTION_NAME); } } } // clang-format off export class AttributeRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.AttributeMatch) { // clang-format on readonly #stylesPane: StylesSidebarPane; readonly #treeElement: StylePropertyTreeElement|null; readonly #matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles; readonly #computedStyles: Map<string, string>; constructor( stylesPane: StylesSidebarPane, treeElement: StylePropertyTreeElement|null, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, computedStyles: Map<string, string>) { super(); this.#treeElement = treeElement; this.#stylesPane = stylesPane; this.#matchedStyles = matchedStyles; this.#computedStyles = computedStyles; } override render(match: SDK.CSSPropertyParserMatchers.AttributeMatch, context: RenderingContext): Node[] { if (this.#treeElement?.property.ownerStyle.parentRule instanceof SDK.CSSRule.CSSFunctionRule) { return Renderer.render(ASTUtils.children(match.node), context).nodes; } const rawValue = match.rawAttributeValue(); const attributeValue = match.resolveAttributeValue(); const fromFallback = attributeValue === null; const attributeMissing = rawValue === null; const typeError = fromFallback && !attributeMissing; const attributeClass = attributeMissing ? 'inactive' : ''; const typeClass = typeError ? 'inactive' : ''; const fallbackClass = fromFallback ? '' : 'inactive'; const computedValue = attributeValue ?? match.fallbackValue(); const varSwatch = document.createElement('span'); const substitution = context.tracing?.substitution({match, context}); if (substitution) { // TODO(b/441945435): If we combine these conditions, we can, when no fallback is // specified but the type check fails, render a series of substitutions // which may help debug why the type check failed. However, we can't // distinguish ordinary type mismatch from cycles, and we need a way // to handle cycles. And we may want UI for showing the substitutions // anyway, even when a fallback is specified. if (fromFallback) { if (match.fallback) { return Renderer.render(match.fallback, substitution.renderingContext(context)).nodes; } } else if (match.substitutionText !== null) { const matching = SDK.CSSPropertyParser.matchDeclaration( '--property', match.substitutionText, this.#matchedStyles.propertyMatchers(match.style, this.#computedStyles)); return Renderer .renderValueNodes( {name: '--property', value: match.substitutionText}, matching, getPropertyRenderers( '--property', match.style, this.#stylesPane, this.#matchedStyles, null, this.#computedStyles), substitution) .nodes; } } const renderedFallback = match.fallback ? Renderer.render(match.fallback, context) : undefined; const attrCall = this.#treeElement?.getTracingTooltip('attr', match.node, this.#matchedStyles, this.#computedStyles, context); const tooltipId = attributeMissing ? undefined : this.#treeElement?.getTooltipId('custom-attribute'); const tooltip = tooltipId ? {tooltipId} : undefined; // clang-format off render(html` <span data-title=${computedValue || ''} jslog=${VisualLogging.link('css-variable').track({click: true, hover: true})} >${attrCall ?? 'attr'}(<devtools-link-swatch class=${attributeClass} .data=${{ tooltip, text: match.name, isDefined: true, onLinkActivate: () => this.#handleAttributeActivate(this.#matchedStyles.originatingNodeForStyle(match.style), match.name), }}></devtools-link-swatch>${tooltipId ? html` <devtools-tooltip id=${tooltipId} variant=rich jslogContext=elements.css-var >${JSON.stringify(rawValue)}</devtools-tooltip>` : nothing}${ match.type ? html` <span class=${typeClass}>${match.type}</span>` : nothing }${renderedFallback ? html`, <span class=${fallbackClass}>${renderedFallback.nodes}</span>` : nothing })</span>`, varSwatch); // clang-format on const color = computedValue && Common.Color.parse(computedValue); if (!color) { return [varSwatch]; } const colorSwatch = new ColorRenderer(this.#stylesPane, this.#treeElement).renderColorSwatch(color, varSwatch); context.addControl('color', colorSwatch); if (fromFallback) { renderedFallback?.cssControls.get('color')?.forEach( innerSwatch => innerSwatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, ev => { colorSwatch.color = ev.data.color; })); } return [colorSwatch, varSwatch]; } #handleAttributeActivate(node: SDK.DOMModel.DOMNode|null, attribute: string): void { if (!node) { return; } Host.userMetrics.actionTaken(Host.UserMetrics.Action.AttributeLinkClicked); Host.userMetrics.swatchActivated(Host.UserMetrics.SwatchType.ATTR_LINK); ElementsPanel.instance().highlightNodeAttribute(node, attribute); } } // clang-format off export class LinearGradientRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.LinearGradientMatch) { // clang-format on override render(match: SDK.CSSPropertyParserMatchers.LinearGradientMatch, context: RenderingContext): Node[] { const children = ASTUtils.children(match.node); const {nodes, cssControls} = Renderer.render(children, context); const angles = cssControls.get('angle'); const angle = angles?.length === 1 ? angles[0] : null; if (angle instanceof InlineEditor.CSSAngle.CSSAngle) { angle.updateProperty(context.matchedResult.getComputedText(match.node)); const args = ASTUtils.callArgs(match.node); const angleNode = args[0]?.find( node => context.matchedResult.getMatch(node) instanceof SDK.CSSPropertyParserMatchers.AngleMatch); const angleMatch = angleNode && context.matchedResult.getMatch(angleNode); if (angleMatch) { angle.addEventListener(InlineEditor.InlineEditorUtils.ValueChangedEvent.eventName, ev => { angle.updateProperty( context.matchedResult.getComputedText(match.node, match => match === angleMatch ? ev.data.value : null)); }); } } return nodes; } } // clang-format off export class RelativeColorChannelRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.RelativeColorChannelMatch) { // clang-format on readonly #treeElement: StylePropertyTreeElement|null; constructor(treeElement: StylePropertyTreeElement|null) { super(); this.#treeElement = treeElement; } override render(match: SDK.CSSPropertyParserMatchers.RelativeColorChannelMatch, context: RenderingContext): Node[] { const color = context.findParent(match.node, SDK.CSSPropertyParserMatchers.ColorMatch); if (!color?.relativeColor) { return [document.createTextNode(match.text)]; } const value = match.getColorChannelValue(color.relativeColor); if (value === null) { return [document.createTextNode(match.text)]; } const evaluation = context.tracing?.applyEvaluation([], () => ({placeholder: [document.createTextNode(value.toFixed(3))]})); if (evaluation) { return evaluation; } const span = document.createElement('span'); span.append(match.text); const tooltipId = this.#treeElement?.getTooltipId('relative-color-channel'); if (!tooltipId) { return [span]; } span.setAttribute('aria-details', tooltipId); const tooltip = new Tooltips.Tooltip.Tooltip({ id: tooltipId, variant: 'rich', anchor: span, jslogContext: 'elements.relative-color-channel', }); tooltip.append(value.toFixed(3)); return [span, tooltip]; } } // clang-format off export class ColorRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.ColorMatch) { // clang-format on readonly #treeElement: StylePropertyTreeElement|null; readonly #stylesPane: StylesSidebarPane; constructor(stylesPane: StylesSidebarPane, treeElement: StylePropertyTreeElement|null) { super(); this.#treeElement = treeElement; this.#stylesPane = stylesPane; } #getValueChild(match: SDK.CSSPropertyParserMatchers.ColorMatch, context: RenderingContext): { valueChild: HTMLSpanElement, cssControls?: SDK.CSSPropertyParser.CSSControlMap, childTracingContexts?: TracingContext[], } { const valueChild = document.createElement('span'); if (match.node.name !== 'CallExpression') { valueChild.appendChild(document.createTextNode(match.text)); return {valueChild}; } const func = context.matchedResult.ast.text(match.node.getChild('Callee')); const args = ASTUtils.siblings(match.node.getChild('ArgList')); const childTracingContexts = context.tracing?.evaluation([args], {match, context}) ?? undefined; const renderingContext = childTracingContexts?.at(0)?.renderingContext(context) ?? context; const {nodes, cssControls} = Renderer.renderInto(args, renderingContext, valueChild); render( html`${ this.#treeElement?.getTracingTooltip( func, match.node, this.#treeElement.matchedStyles(), this.#treeElement.getComputedStyles() ?? new Map(), renderingContext) ?? func}${nodes}`, valueChild); return {valueChild, cssControls, childTracingContexts}; } override render(match: SDK.CSSPropertyParserMatchers.ColorMatch, context: RenderingContext): Node[] { const {valueChild, cssControls, childTracingContexts} = this.#getValueChild(match, context); let colorText = context.matchedResult.getComputedText(match.node); if (match.relativeColor) { const fakeSpan = document.body.appendChild(document.createElement('span')); fakeSpan.style.backgroundColor = colorText; colorText = window.getComputedStyle(fakeSpan).backgroundColor?.toString() || colorText; fakeSpan.remove(); } // Now try render a color swatch if the result is parsable. const color = Common.Color.parse(colorText); if (!color) { if (match.node.name === 'CallExpression') { return Renderer.render(ASTUtils.children(match.node), context).nodes; } return [document.createTextNode(colorText)]; } if (match.node.name === 'CallExpression' && childTracingContexts) { const evaluation = context.tracing?.applyEvaluation(childTracingContexts, () => { const displayColor = color.as(((color.alpha ?? 1) !== 1) ? Common.Color.Format.HEXA : Common.Color.Format.HEX); const colorText = document.createElement('span'); colorText.textContent = displayColor.asString(); const swatch = new ColorRenderer(this.#stylesPane, null) .renderColorSwatch( displayColor.isGamutClipped() ? color : (displayColor.nickname() ?? displayColor), colorText); swatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, ev => { colorText.textContent = ev.data.color.asString(); }); context.addControl('color', swatch); return {placeholder: [swatch, colorText]}; }); if (evaluation) { return evaluation; } } const swatch = this.renderColorSwatch(color, valueChild); context.addControl('color', swatch); // For hsl/hwb colors, hook up the angle swatch for the hue. if (cssControls && match.node.name === 'CallExpression' && context.ast.text(match.node.getChild('Callee')).match(/^(hsla?|hwba?)/)) { const [angle] = cssControls.get('angle') ?? []; if (angle instanceof InlineEditor.CSSAngle.CSSAngle) { angle.updateProperty(swatch.color?.asString() ?? ''); angle.addEventListener(InlineEditor.InlineEditorUtils.ValueChangedEvent.eventName, ev => { const hue = Common.Color.parseHueNumeric(ev.data.value); const color = swatch.color; if (!hue || !color) { return; } if (color.is(Common.Color.Format.HSL) || color.is(Common.Color.Format.HSLA)) { swatch.color = new Common.Color.HSL(hue, color.s, color.l, color.alpha); } else if (color.is(Common.Color.Format.HWB) || color.is(Common.Color.Format.HWBA)) { swatch.color = new Common.Color.HWB(hue, color.w, color.b, color.alpha); } angle.updateProperty(swatch.color?.asString() ?? ''); }); } } return [swatch, valueChild]; } renderColorSwatch(color: Common.Color.Color|undefined, valueChild: Node): InlineEditor.ColorSwatch.ColorSwatch { const editable = this.#treeElement?.editable(); const shiftClickMessage = i18nString(UIStrings.shiftClickToChangeColorFormat); const tooltip = editable ? i18nString(UIStrings.openColorPickerS, {PH1: shiftClickMessage}) : ''; const swatch = new InlineEditor.ColorSwatch.ColorSwatch(tooltip); swatch.readonly = !editable; if (color) { swatch.color = color; } if (this.#treeElement?.editable()) { const treeElement = this.#treeElement; const onColorChanged = (): void => { void treeElement.applyStyleText(treeElement.renderedPropertyText(), false); }; const onColorFormatChanged = (e: InlineEditor.ColorSwatch.ColorFormatChangedEvent): void => { valueChild.textContent = e.data.color.getAuthoredText() ?? e.data.color.asString(); void treeElement.applyStyleText(treeElement.renderedPropertyText(), false); }; swatch.addEventListener(InlineEditor.ColorSwatch.ClickEvent.eventName, () => { Host.userMetrics.swatchActivated(Host.UserMetrics.SwatchType.COLOR); }); swatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, onColorChanged); swatch.addEventListener(InlineEditor.ColorSwatch.ColorFormatChangedEvent.eventName, onColorFormatChanged); const swatchIcon = new ColorSwatchPopoverIcon(treeElement, treeElement.parentPane().swatchPopoverHelper(), swatch); swatchIcon.addEventListener(ColorSwatchPopoverIconEvents.COLOR_CHANGED, ev => { valueChild.textContent = ev.data.getAuthoredText() ?? ev.data.asString(); swatch.color = ev.data; }); if (treeElement.property.name === 'color') { void this.#addColorContrastInfo(swatchIcon); } } return swatch; } async #addColorContrastInfo(swatchIcon: ColorSwatchPopoverIcon): Promise<void> { const cssModel = this.#stylesPane.cssModel(); const node = this.#stylesPane.node(); if (!cssModel || typeof node?.id === 'undefined') { return; } const contrastInfo = new ColorPicker.ContrastInfo.ContrastInfo(await cssModel.getBackgroundColors(node.id)); swatchIcon.setContrastInfo(contrastInfo); } } // clang-format off export class LightDarkColorRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.LightDarkColorMatch) { // clang-format on readonly #treeElement: StylePropertyTreeElement|null; readonly #stylesPane: StylesSidebarPane; readonly #matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles; constructor( stylesPane: StylesSidebarPane, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, treeElement: StylePropertyTreeElement|null) { super(); this.#treeElement = treeElement; this.#stylesPane = stylesPane; this.#matchedStyles = matchedStyles; } override render(match: SDK.CSSPropertyParserMatchers.LightDarkColorMatch, context: RenderingContext): Node[] { const content = document.createElement('span'); content.appendChild(document.createTextNode('light-dark(')); const light = content.appendChild(document.createElement('span')); content.appendChild(document.createTextNode(', ')); const dark = content.appendChild(document.createElement('span')); content.appendChild(document.createTextNode(')')); const {cssControls: lightControls} = Renderer.renderInto(match.light, context, light); const {cssControls: darkControls} = Renderer.renderInto(match.dark, context, dark); if (context.matchedResult.hasUnresolvedSubstitutions(match.node)) { return [content]; } const color = Common.Color.parse( context.matchedResult.getComputedTextRange(match.light[0], match.light[match.light.length - 1])); if (!color) { return [content]; } // Pass an undefined color here to insert a placeholder swatch that will be filled in from the async // applyColorScheme below. const colorSwatch = new ColorRenderer(this.#stylesPane, this.#treeElement).renderColorSwatch(undefined, content); context.addControl('color', colorSwatch); void this.applyColorScheme(match, context, colorSwatch, light, dark, lightControls, darkControls); return [colorSwatch, content]; } async applyColorScheme( match: SDK.CSSPropertyParserMatchers.LightDarkColorMatch, context: RenderingContext, colorSwatch: InlineEditor.ColorSwatch.ColorSwatch, light: HTMLSpanElement, dark: HTMLSpanElement, lightControls: SDK.CSSPropertyParser.CSSControlMap, darkControls: SDK.CSSPropertyParser.CSSControlMap): Promise<void> { const activeColor = await this.#activeColor(match); if (!activeColor) { return; } const activeColorSwatches = (activeColor === match.light ? lightControls : darkControls).get('color'); activeColorSwatches?.forEach( swatch => swatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, ev => { colorSwatch.color = ev.data.color; })); const inactiveColor = (activeColor === match.light) ? dark : light; const colorText = context.matchedResult.getComputedTextRange(activeColor[0], activeColor[activeColor.length - 1]); const color = colorText && Common.Color.parse(colorText); inactiveColor.classList.add('inactive-value'); if (color) { colorSwatch.color = color; } } // Returns the syntax node group corresponding the active color scheme: // If the element has color-scheme set to light or dark, return the respective group. // If the element has color-scheme set to both light and dark, we check the prefers-color-scheme media query. async #activeColor(match: SDK.CSSPropertyParserMatchers.LightDarkColorMatch): Promise<CodeMirror.SyntaxNode[]|undefined> { const activeColorSchemes = this.#matchedStyles.resolveProperty('color-scheme', match.style) ?.parseValue(this.#matchedStyles, new Map()) ?.getComputedPropertyValueText() .split(' ') ?? []; const hasLight = activeColorSchemes.includes(SDK.CSSModel.ColorScheme.LIGHT); const hasDark = activeColorSchemes.includes(SDK.CSSModel.ColorScheme.DARK); if (!hasDark && !hasLight) { return match.light; } if (!hasLight) { return match.dark; } if (!hasDark) { return match.light; } switch (await this.#stylesPane.cssModel()?.colorScheme()) { case SDK.CSSModel.ColorScheme.DARK: return match.dark; case SDK.CSSModel.ColorScheme.LIGHT: return match.light; default: return undefined; } } } // clang-format off export class ColorMixRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.ColorMixMatch) { // clang-format on readonly #pane: StylesSidebarPane; readonly #matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles; readonly #computedStyles: Map<string, string>; readonly #treeElement: StylePropertyTreeElement|null; constructor( pane: StylesSidebarPane, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, computedStyles: Map<string, string>, treeElement: StylePropertyTreeElement|null) { super(); this.#pane = pane; this.#matchedStyles = matchedStyles; this.#computedStyles = computedStyles; this.#treeElement = treeElement; } override render(match: SDK.CSSPropertyParserMatchers.ColorMixMatch, context: RenderingContext): Node[] { const hookUpColorArg = (node: Node, onChange: (newColorText: string) => void): boolean => { if (node instanceof InlineEditor.ColorMixSwatch.ColorMixSwatch || node instanceof InlineEditor.ColorSwatch.ColorSwatch) { if (node instanceof InlineEditor.ColorSwatch.ColorSwatch) { node.addEventListener( InlineEditor.ColorSwatch.ColorChangedEvent.eventName, ev => onChange(ev.data.color.getAuthoredText() ?? ev.data.color.asString())); } else { node.addEventListener( InlineEditor.ColorMixSwatch.ColorMixChangedEvent.eventName, ev => onChange(ev.data.text)); } const color = node.getText(); if (color) { onChange(color); return true; } } return false; }; const childTracingContexts = context.tracing?.evaluation([match.space, match.color1, match.color2], {match, context}); const childRenderingContexts = childTracingContexts?.map(ctx => ctx.renderingContext(context)) ?? [context, context, context]; const contentChild = document.createElement('span'); const color1 = Renderer.renderInto(match.color1, childRenderingContexts[1], contentChild); const color2 = Renderer.renderInto(match.color2, childRenderingContexts[2], contentChild); render( html`${ this.#treeElement?.getTracingTooltip( 'color-mix', match.node, this.#matchedStyles, this.#computedStyles, context) ?? 'color-mix'}(${Renderer.render(match.space, childRenderingContexts[0]).nodes}, ${color1.nodes}, ${ color2.nodes})`, contentChild); const color1Controls = color1.cssControls.get('color') ?? []; const color2Controls = color2.cssControls.get('color') ?? []; if (context.matchedResult.hasUnresolvedSubstitutions(match.node) || color1Controls.length !== 1 || color2Controls.length !== 1) { return [contentChild]; } const space = match.space.map(space => context.matchedResult.getComputedText(space)).join(' '); const color1Text = match.color1.map(color => context.matchedResult.getComputedText(color)).join(' '); const color2Text = match.color2.map(color => context.matchedResult.getComputedText(color)).join(' '); const colorMixText = `color-mix(${space}, ${color1Text}, ${color2Text})`; const nodeId = this.#pane.node()?.id; if (nodeId !== undefined && childTracingContexts) { const evaluation = context.tracing?.applyEvaluation(childTracingContexts, () => { const initialColor = Common.Color.parse('#000') as Common.Color.Color; const colorText = document.createElement('span'); colorText.textContent = initialColor.asString(); const swatch = new ColorRenderer(this.#pane, null).renderColorSwatch(initialColor, colorText); swatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, ev => { colorText.textContent = ev.data.color.asString(); }); context.addControl('color', swatch); const asyncEvalCallback = async(): Promise<boolean> => { const results = await this.#pane.cssModel()?.resolveValues(undefined, nodeId, colorMixText); if (results) { const color = Common.Color.parse(results[0]); if (color) { swatch.color = color.as(Common.Color.Format.HEXA); return true; } } return false; }; return {placeholder: [swatch, colorText], asyncEvalCallback}; }); if (evaluation) { return evaluation; } } const swatch = new InlineEditor.ColorMixSwatch.ColorMixSwatch(); if (!hookUpColorArg(color1Controls[0], text => swatch.setFirstColor(text)) || !hookUpColorArg(color2Controls[0], text => swatch.setSecondColor(text))) { return [contentChild]; } swatch.tabIndex = -1; swatch.setColorMixText(colorMixText); UI.ARIAUtils.setLabel(swatch, colorMixText); context.addControl('color', swatch); if (context.tracing) { return [swatch, contentChild]; } const tooltipId = this.#treeElement?.getTooltipId('color-mix'); if (!tooltipId) { return [swatch, contentChild]; } swatch.setAttribute('aria-details', tooltipId); const tooltip = new Tooltips.Tooltip.Tooltip({ id: tooltipId, variant: 'rich', anchor: swatch, jslogContext: 'elements.css-color-mix', }); const colorTextSpan = tooltip.appendChild(document.createElement('span')); tooltip.onbeforetoggle = e => { if ((e as ToggleEvent).newState !== 'open') { return; } const color = swatch.mixedColor(); if (!color) { return; } const rgb = color.as(Common.Color.Format.HEX); colorTextSpan.textContent = rgb.isGamutClipped() ? color.asString() : rgb.asString(); }; return [swatch, contentChild, tooltip]; } } // clang-format off export class AngleRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.AngleMatch) { // clang-format on readonly #treeElement: StylePropertyTreeElement|null; constructor(treeElement: StylePropertyTreeElement|null) { super(); this.#treeElement = treeElement; } override render(match: SDK.CSSPropertyParserMatchers.AngleMatch, context: RenderingContext): Node[] { const angleText = match.text; if (!this.#treeElement?.editable()) { return [document.createTextNode(angleText)]; } const cssAngle = new InlineEditor.CSSAngle.CSSAngle(); cssAngle.setAttribute('jslog', `${VisualLogging.showStyleEditor().track({click: true}).context('css-angle')}`); const valueElement = document.createElement('span'); valueElement.textContent = angleText; cssAngle.data = { angleText, containingPane: (this.#treeElement.parentPane().element.enclosingNodeOrSelfWithClass('style-panes-wrapper') as HTMLElement), }; cssAngle.append(valueElement); const treeElement = this.#treeElement; cssAngle.addEventListener('popovertoggled', ({data}) => { const section = treeElement.section(); if (!section) { return; } if (data.open) { treeElement.parentPane().hideAllPopovers(); treeElement.parentPane().activeCSSAngle = cssAngle; Host.userMetrics.swatchActivated(Host.UserMetrics.SwatchType.ANGLE); } section.element.classList.toggle('has-open-popover', data.open); treeElement.parentPane().setEditingStyle(data.open); // Commit the value as a major change after the angle popover is closed. if (!data.open) { void treeElement.applyStyleText(treeElement.renderedPropertyText(), true); } }); cssAngle.addEventListener('valuechanged', async ({data}) => { valueElement.textContent = data.value; await treeElement.applyStyleText(treeElement.renderedPropertyText(), false); }); cssAngle.addEventListener('unitchanged', ({data}) => { valueElement.textContent = data.value; }); context.addControl('angle', cssAngle); return [cssAngle]; } } // clang-format off export class LinkableNameRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.LinkableNameMatch) { // clang-format on readonly #matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles; readonly #stylesPane: StylesSidebarPane; constructor(matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles, stylesSidebarPane: StylesSidebarPane) { super(); this.#matchedStyles = matchedStyles; this.#stylesPane = stylesSidebarPane; } #getLinkData(match: SDK.CSSPropertyParserMatchers.LinkableNameMatch): {jslogContext: string, metric: null|Host.UserMetrics.SwatchType, ruleBlock: string, isDefined: boolean} { switch (match.propertyName) { case SDK.CSSPropertyParserMatchers.LinkableNameProperties.ANIMATION: case SDK.CSSPropertyParserMatchers.LinkableNameProperties.ANIMATION_NAME: return { jslogContext: 'css-animation-name', metric: Host.UserMetrics.SwatchType.ANIMATION_NAME_LINK, ruleBlock: '@keyframes', isDefined: Boolean(this.#matchedStyles.keyframes().find(kf => kf.name().text === match.text)), }; case SDK.CSSPropertyParserMatchers.LinkableNameProperties.FONT_PALETTE: return { jslogContext: 'css-font-palette', metric: null, ruleBlock: '@font-*', isDefined: Boolean(this.#matchedStyles.atRules().find( ar => ar.type() === 'font-palette-values' && ar.name()?.text === match.text)), }; case SDK.CSSPropertyParserMatchers.LinkableNameProperties.POSITION_TRY: case SDK.CSSPropertyParserMatchers.LinkableNameProperties.POSITION_TRY_FALLBACKS: return { jslogContext: 'css-position-try', metric: Host.UserMetrics.SwatchType.POSITION_TRY_LINK, ruleBlock: '@position-try', isDefined: Boolean(this.#matchedStyles.positionTryRules().find(pt => pt.name().text === match.text)), }; } } override render(match: SDK.CSSPropertyParserMatchers.LinkableNameMatch): Node[] { const swatch = new InlineEditor.LinkSwatch.LinkSwatch(); const {metric, jslogContext, ruleBlock, isDefined} = this.#getLinkData(match); swatch.data = { text: match.text, tooltip: isDefined ? undefined : {title: i18nString(UIStrings.sIsNotDefined, {PH1: match.text})}, isDefined, onLinkActivate: (): void => { metric && Host.userMetrics.swatchActivated(metric); if (match.propertyName === SDK.CSSPropertyParserMatchers.LinkableNameProperties.FONT_PALETTE) { this.#stylesPane.jumpToFontPaletteDefinition(match.text); } else { this.#stylesPane.jumpToSectionBlock(`${ruleBlock} ${match.text}`); } }, jslogContext, }; if (match.propertyName === SDK.CSSPropertyParserMatchers.LinkableNameProperties.ANIMATION || match.propertyName === SDK.CSSPropertyParserMatchers.LinkableNameProperties.ANIMATION_NAME) { const el = document.createElement('span'); el.appendChild(swatch); const node = this.#stylesPane.node(); if (node) { const animationModel = node.domModel().target().model(SDK.AnimationModel.AnimationModel); void animationModel?.getAnimationGroupForAnimation(match.text, node.id).then(maybeAnimationGroup => { if (!maybeAnimationGroup) { return; } const icon = createIcon('animation', 'open-in-animations-panel'); icon.setAttribute('jslog', `${VisualLogging.link('open-in-animations-panel').track({click: true})}`); icon.setAttribute('role', 'button'); icon.setAttribute('title', i18nString(UIStrings.jumpToAnimationsPanel)); icon.addEventListener('mouseup', ev => { ev.consume(true); void Common.Revealer.reveal(maybeAnimationGroup); }); el.insertBefore(icon, swatch); }); } return [el]; } return [swatch]; } } // clang-format off export class BezierRenderer extends rendererBase(SDK.CSSPropertyParserMatchers.BezierMatch) { // clang-format on readonly #treeElement: StylePropertyTreeElement|null; constructor(treeElement: StylePropertyTreeElement|null) { super(); this.#treeElement = treeElement; } override render(match: SDK.CSSPropertyParserMatchers.BezierMatch, context: RenderingContext): Node[] { const nodes = match.node.name === 'CallExpression' ? Renderer.render(ASTUtils.children(match.node), context).nodes : [document.createTextNode(match.text)]; if (!this.#treeElement?.editable() || !InlineEditor.AnimationTimingModel.AnimationTimingModel.parse( context.matchedResult.getComputedText(match.node))) { return nodes; } const swatchPopoverHelper = this.#treeElement.parentPane().swatchPopoverHelper(); const icon = createIcon('bezier-curve-filled', 'bezier-swatch-icon'); icon.s