Skip to content

Commit 3bdc229

Browse files
Eric LeeseDevtools-frontend LUCI CQ
authored andcommitted
Display css function rules in Elements panel
Bug: 392034817 Change-Id: I589055e527364996c346b37bb760c654a39bc1fd Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6308828 Commit-Queue: Eric Leese <[email protected]> Reviewed-by: Changhao Han <[email protected]>
1 parent 7099949 commit 3bdc229

File tree

9 files changed

+284
-14
lines changed

9 files changed

+284
-14
lines changed

front_end/core/sdk/CSSMatchedStyles.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
} from './CSSPropertyParserMatchers.js';
3636
import {
3737
CSSFontPaletteValuesRule,
38+
CSSFunctionRule,
3839
CSSKeyframesRule,
3940
CSSPositionTryRule,
4041
CSSPropertyRule,
@@ -205,6 +206,7 @@ export interface CSSMatchedStylesPayload {
205206
animationStylesPayload: Protocol.CSS.CSSAnimationStyle[];
206207
transitionsStylePayload: Protocol.CSS.CSSStyle|null;
207208
inheritedAnimatedPayload: Protocol.CSS.InheritedAnimatedStyleEntry[];
209+
functionRules: Protocol.CSS.CSSFunctionRule[];
208210
}
209211

210212
export class CSSRegisteredProperty {
@@ -289,6 +291,7 @@ export class CSSMatchedStyles {
289291
#mainDOMCascade?: DOMInheritanceCascade;
290292
#pseudoDOMCascades?: Map<Protocol.DOM.PseudoType, DOMInheritanceCascade>;
291293
#customHighlightPseudoDOMCascades?: Map<string, DOMInheritanceCascade>;
294+
#functionRules: CSSFunctionRule[];
292295
readonly #fontPaletteValuesRule: CSSFontPaletteValuesRule|undefined;
293296

294297
static async create(payload: CSSMatchedStylesPayload): Promise<CSSMatchedStyles> {
@@ -307,6 +310,7 @@ export class CSSMatchedStyles {
307310
cssPropertyRegistrations,
308311
fontPaletteValuesRule,
309312
activePositionFallbackIndex,
313+
functionRules,
310314
}: CSSMatchedStylesPayload) {
311315
this.#cssModelInternal = cssModel;
312316
this.#nodeInternal = node;
@@ -323,6 +327,7 @@ export class CSSMatchedStyles {
323327
fontPaletteValuesRule ? new CSSFontPaletteValuesRule(cssModel, fontPaletteValuesRule) : undefined;
324328

325329
this.#activePositionFallbackIndex = activePositionFallbackIndex;
330+
this.#functionRules = functionRules.map(rule => new CSSFunctionRule(cssModel, rule));
326331
}
327332

328333
private async init({
@@ -759,6 +764,10 @@ export class CSSMatchedStyles {
759764
return this.#registeredPropertyMap.get(name);
760765
}
761766

767+
functionRules(): CSSFunctionRule[] {
768+
return this.#functionRules;
769+
}
770+
762771
fontPaletteValuesRule(): CSSFontPaletteValuesRule|undefined {
763772
return this.#fontPaletteValuesRule;
764773
}

front_end/core/sdk/CSSModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ export class CSSModel extends SDKModel<EventTypes> {
392392
parentLayoutNodeId: matchedStylesResponse.parentLayoutNodeId,
393393
positionTryRules: matchedStylesResponse.cssPositionTryRules || [],
394394
propertyRules: matchedStylesResponse.cssPropertyRules ?? [],
395+
functionRules: matchedStylesResponse.cssFunctionRules ?? [],
395396
cssPropertyRegistrations: matchedStylesResponse.cssPropertyRegistrations ?? [],
396397
fontPaletteValuesRule: matchedStylesResponse.cssFontPaletteValuesRule,
397398
activePositionFallbackIndex: matchedStylesResponse.activePositionFallbackIndex ?? -1,

front_end/core/sdk/CSSRule.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,78 @@ export class CSSPositionTryRule extends CSSRule {
351351
return this.#active;
352352
}
353353
}
354+
355+
export interface CSSNestedStyleLeaf {
356+
style: CSSStyleDeclaration;
357+
}
358+
359+
export type CSSNestedStyleCondition = {
360+
children: CSSNestedStyle[],
361+
}&({media: CSSMedia}|{container: CSSContainerQuery}|{supports: CSSSupports});
362+
363+
export type CSSNestedStyle = CSSNestedStyleLeaf|CSSNestedStyleCondition;
364+
365+
export class CSSFunctionRule extends CSSRule {
366+
readonly #name: CSSValue;
367+
readonly #parameters: string[];
368+
readonly #children: CSSNestedStyle[];
369+
constructor(cssModel: CSSModel, payload: Protocol.CSS.CSSFunctionRule) {
370+
super(
371+
cssModel,
372+
{origin: payload.origin, style: {cssProperties: [], shorthandEntries: []}, styleSheetId: payload.styleSheetId});
373+
this.#name = new CSSValue(payload.name);
374+
this.#parameters = payload.parameters.map(({name}) => name);
375+
this.#children = this.protocolNodesToNestedStyles(payload.children);
376+
}
377+
378+
functionName(): CSSValue {
379+
return this.#name;
380+
}
381+
382+
parameters(): string[] {
383+
return this.#parameters;
384+
}
385+
386+
children(): CSSNestedStyle[] {
387+
return this.#children;
388+
}
389+
390+
protocolNodesToNestedStyles(nodes: Protocol.CSS.CSSFunctionNode[]): CSSNestedStyle[] {
391+
const result = [];
392+
for (const node of nodes) {
393+
const nestedStyle = this.protocolNodeToNestedStyle(node);
394+
if (nestedStyle) {
395+
result.push(nestedStyle);
396+
}
397+
}
398+
return result;
399+
}
400+
401+
protocolNodeToNestedStyle(node: Protocol.CSS.CSSFunctionNode): CSSNestedStyle|undefined {
402+
if (node.style) {
403+
return {style: new CSSStyleDeclaration(this.cssModelInternal, this, node.style, Type.Regular)};
404+
}
405+
if (node.condition) {
406+
const children = this.protocolNodesToNestedStyles(node.condition.children);
407+
if (node.condition.media) {
408+
return {children, media: new CSSMedia(this.cssModelInternal, node.condition.media)};
409+
}
410+
if (node.condition.containerQueries) {
411+
return {
412+
children,
413+
container: new CSSContainerQuery(this.cssModelInternal, node.condition.containerQueries),
414+
};
415+
}
416+
if (node.condition.supports) {
417+
return {
418+
children,
419+
supports: new CSSSupports(this.cssModelInternal, node.condition.supports),
420+
};
421+
}
422+
console.error('A function rule condition must have a media, container, or supports');
423+
return;
424+
}
425+
console.error('A function rule node must have a style or condition');
426+
return;
427+
}
428+
}

front_end/panels/elements/StylePropertiesSection.ts

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,6 @@ export class StylePropertiesSection {
786786
let supportsIndex = 0;
787787
let nestingIndex = 0;
788788
this.nestingLevel = 0;
789-
const indent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get();
790789
for (const ruleType of rule.ruleTypes) {
791790
let ancestorRuleElement;
792791
switch (ruleType) {
@@ -808,11 +807,7 @@ export class StylePropertiesSection {
808807
}
809808
if (ancestorRuleElement) {
810809
this.#ancestorRuleListElement.prepend(ancestorRuleElement);
811-
const closingBrace = document.createElement('div');
812-
closingBrace.createChild('span', 'styles-clipboard-only').textContent = indent.repeat(this.nestingLevel);
813-
closingBrace.style.paddingLeft = `${this.nestingLevel}ch`;
814-
closingBrace.append('}');
815-
this.#ancestorClosingBracesElement.prepend(closingBrace);
810+
this.#ancestorClosingBracesElement.prepend(this.indentElement(this.createClosingBrace(), this.nestingLevel));
816811
this.nestingLevel++;
817812
}
818813
}
@@ -824,16 +819,30 @@ export class StylePropertiesSection {
824819

825820
let curNestingLevel = 0;
826821
for (const element of this.#ancestorRuleListElement.children) {
827-
const indentElement = document.createElement('span');
828-
indentElement.classList.add('styles-clipboard-only');
829-
indentElement.setAttribute('slot', 'indent');
830-
indentElement.textContent = indent.repeat(curNestingLevel);
831-
element.prepend(indentElement);
832-
(element as HTMLElement).style.paddingLeft = `${curNestingLevel}ch`;
822+
this.indentElement(element as HTMLElement, curNestingLevel);
833823
curNestingLevel++;
834824
}
835825
}
836826

827+
protected createClosingBrace(): HTMLElement {
828+
const closingBrace = document.createElement('div');
829+
closingBrace.append('}');
830+
return closingBrace;
831+
}
832+
833+
protected indentElement(element: HTMLElement, nestingLevel: number, clipboardOnly?: boolean): HTMLElement {
834+
const indent = Common.Settings.Settings.instance().moduleSetting('text-editor-indent').get();
835+
const indentElement = document.createElement('span');
836+
indentElement.classList.add('styles-clipboard-only');
837+
indentElement.setAttribute('slot', 'indent');
838+
indentElement.textContent = indent.repeat(nestingLevel);
839+
element.prepend(indentElement);
840+
if (!clipboardOnly) {
841+
element.style.paddingLeft = `${nestingLevel}ch`;
842+
}
843+
return element;
844+
}
845+
837846
protected createMediaElement(media: SDK.CSSMedia.CSSMedia): ElementsComponents.CSSQuery.CSSQuery|undefined {
838847
// Don't display trivial non-print media types.
839848
const isMedia = !media.text || !media.text.includes('(') && media.text !== 'print';
@@ -1072,7 +1081,10 @@ export class StylePropertiesSection {
10721081
this.parentPane.setActiveProperty(null);
10731082
this.nextEditorTriggerButtonIdx = 1;
10741083
this.propertiesTreeOutline.removeChildren();
1075-
const style = this.styleInternal;
1084+
this.populateStyle(this.styleInternal, this.propertiesTreeOutline);
1085+
}
1086+
1087+
populateStyle(style: SDK.CSSStyleDeclaration.CSSStyleDeclaration, parent: TreeElementParent): void {
10761088
let count = 0;
10771089
const properties = style.leadingProperties();
10781090
const maxProperties = DEFAULT_MAX_PROPERTIES + properties.length - this.originalPropertiesCount;
@@ -1100,7 +1112,7 @@ export class StylePropertiesSection {
11001112
});
11011113
item.setComputedStyles(this.computedStyles);
11021114
item.setParentsComputedStyles(this.parentsComputedStyles);
1103-
this.propertiesTreeOutline.appendChild(item);
1115+
parent.appendChild(item);
11041116
}
11051117

11061118
if (count < properties.length) {
@@ -1750,6 +1762,68 @@ export class RegisteredPropertiesSection extends StylePropertiesSection {
17501762
}
17511763
}
17521764

1765+
export class FunctionRuleSection extends StylePropertiesSection {
1766+
constructor(
1767+
stylesPane: StylesSidebarPane, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles,
1768+
style: SDK.CSSStyleDeclaration.CSSStyleDeclaration, children: SDK.CSSRule.CSSNestedStyle[], sectionIdx: number,
1769+
functionName: string, parameters: string[], expandedByDefault: boolean) {
1770+
super(stylesPane, matchedStyles, style, sectionIdx, null, null, `${functionName}(${parameters.join(', ')})`);
1771+
if (!expandedByDefault) {
1772+
this.element.classList.add('hidden');
1773+
}
1774+
this.selectorElement.className = 'function-key';
1775+
this.addChildren(children, this.propertiesTreeOutline);
1776+
}
1777+
1778+
createConditionElement(condition: SDK.CSSRule.CSSNestedStyleCondition): HTMLElement|undefined {
1779+
if ('media' in condition) {
1780+
return this.createMediaElement(condition.media);
1781+
}
1782+
if ('container' in condition) {
1783+
return this.createContainerQueryElement(condition.container);
1784+
}
1785+
if ('supports' in condition) {
1786+
return this.createSupportsElement(condition.supports);
1787+
}
1788+
return;
1789+
}
1790+
1791+
positionNestingElement(element: HTMLElement): HTMLElement {
1792+
// Add this class to get the same margins as a property and syntax highlighting.
1793+
element.classList.add('css-function-inline-block');
1794+
// Also add the clipboard text, but don't add additional margins because
1795+
// the tree nesting takes care of that.
1796+
return this.indentElement(element, this.nestingLevel, true);
1797+
}
1798+
1799+
addChildren(children: SDK.CSSRule.CSSNestedStyle[], parent: TreeElementParent): void {
1800+
for (const child of children) {
1801+
if ('style' in child) {
1802+
this.populateStyle(child.style, parent);
1803+
} else if ('children' in child) {
1804+
const conditionElement = this.createConditionElement(child);
1805+
let newParent = parent;
1806+
this.nestingLevel++;
1807+
if (conditionElement) {
1808+
const treeElement = new UI.TreeOutline.TreeElement();
1809+
treeElement.listItemElement.appendChild(this.positionNestingElement(conditionElement));
1810+
treeElement.setExpandable(true);
1811+
treeElement.setCollapsible(false);
1812+
parent.appendChild(treeElement);
1813+
newParent = treeElement;
1814+
}
1815+
this.addChildren(child.children, newParent);
1816+
if (conditionElement) {
1817+
const treeElement = new UI.TreeOutline.TreeElement();
1818+
treeElement.listItemElement.appendChild(this.positionNestingElement(this.createClosingBrace()));
1819+
parent.appendChild(treeElement);
1820+
}
1821+
this.nestingLevel--;
1822+
}
1823+
}
1824+
}
1825+
}
1826+
17531827
export class FontPaletteValuesRuleSection extends StylePropertiesSection {
17541828
constructor(
17551829
stylesPane: StylesSidebarPane, matchedStyles: SDK.CSSMatchedStyles.CSSMatchedStyles,
@@ -1832,3 +1906,7 @@ export class HighlightPseudoStylePropertiesSection extends StylePropertiesSectio
18321906
return false;
18331907
}
18341908
}
1909+
1910+
interface TreeElementParent {
1911+
appendChild(child: UI.TreeOutline.TreeElement): void;
1912+
}

front_end/panels/elements/StylePropertyHighlighter.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describeWithMockConnection('StylePropertyHighlighter', () => {
3838
animationStylesPayload: [],
3939
transitionsStylePayload: null,
4040
inheritedAnimatedPayload: [],
41+
functionRules: [],
4142
});
4243
return {
4344
stylesSidebarPane,

front_end/panels/elements/StylesSidebarPane.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,77 @@ describe('StylesSidebarPane', () => {
120120
assert.instanceOf(sectionBlocks[1].sections[0], Elements.StylePropertiesSection.FontPaletteValuesRuleSection);
121121
});
122122

123+
it('should add @function section to the end', async () => {
124+
const stylesSidebarPane =
125+
new Elements.StylesSidebarPane.StylesSidebarPane(new Elements.ComputedStyleModel.ComputedStyleModel());
126+
const matchedStyles = await getMatchedStyles({
127+
cssModel: stylesSidebarPane.cssModel() as SDK.CSSModel.CSSModel,
128+
node: sinon.createStubInstance(SDK.DOMModel.DOMNode),
129+
functionRules: [{
130+
name: {text: '--f'},
131+
parameters: [{name: '--x', type: '*'}, {name: '--y', type: '*'}],
132+
origin: Protocol.CSS.StyleSheetOrigin.Regular,
133+
children: [
134+
{
135+
condition: {
136+
media: {
137+
text: '(width > 400px)',
138+
source: Protocol.CSS.CSSMediaSource.MediaRule,
139+
},
140+
conditionText: '<unused>',
141+
children: [
142+
{
143+
condition: {
144+
containerQueries: {
145+
text: '(width > 300px)',
146+
},
147+
conditionText: '<unused>',
148+
children: [
149+
{
150+
condition: {
151+
supports: {
152+
text: '(color: red)',
153+
active: true,
154+
},
155+
conditionText: '<unused>',
156+
children: [
157+
{
158+
style: {
159+
cssProperties: [{name: 'result', value: 'var(--y)'}],
160+
shorthandEntries: [],
161+
}
162+
},
163+
],
164+
},
165+
},
166+
],
167+
},
168+
},
169+
],
170+
},
171+
},
172+
{
173+
style: {
174+
cssProperties: [{name: 'result', value: 'var(--x)'}],
175+
shorthandEntries: [],
176+
}
177+
},
178+
],
179+
}],
180+
});
181+
182+
const sectionBlocks =
183+
await stylesSidebarPane.rebuildSectionsForMatchedStyleRulesForTest(matchedStyles, new Map(), new Map());
184+
185+
assert.lengthOf(sectionBlocks, 2);
186+
assert.strictEqual(sectionBlocks[1].titleElement()?.textContent, '@function');
187+
assert.lengthOf(sectionBlocks[1].sections, 1);
188+
assert.instanceOf(sectionBlocks[1].sections[0], Elements.StylePropertiesSection.FunctionRuleSection);
189+
assert.strictEqual(
190+
sectionBlocks[1].sections[0].element.deepTextContent().replaceAll(/\s+/g, ' ').trim(),
191+
'--f(--x, --y) { @media (width > 400px) { @container (width > 300px) { @supports (color: red) { result: var(--y); } } } result: var(--x); }');
192+
});
193+
123194
describe('Animation styles', () => {
124195
function mockGetAnimatedComputedStyles(response: Partial<Protocol.CSS.GetAnimatedStylesForNodeResponse>) {
125196
setMockConnectionResponseHandler('CSS.getAnimatedStylesForNode', () => response);

0 commit comments

Comments
 (0)