blob: d03c5218b078c371d16da43856ea7f0a4c48d864 [file]
// 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 Platform from '../../core/platform/platform.js';
import type * as SDK from '../../core/sdk/sdk.js';
import type * as ChangeTracker from '../change_tracker/change_tracker.js';
import {buildAnchor, trimValue} from './ChangeTrackingUtils.js';
const STYLES_PANE_VE_PATH = 'Panel: elements > Pane: styles';
function formatRuleSuffix(preposition: 'in'|'to'|'from', ruleHeader?: string): string {
const trimmedRule = ruleHeader ? Platform.StringUtilities.collapseWhitespace(ruleHeader.trim()) : '';
return trimmedRule ? ` ${preposition} rule "${trimValue(trimmedRule)}"` : '';
}
export interface StylePropertyEdit {
/** Rule header or selector containing the property (e.g. 'element.style' or '.btn:hover'). */
ruleHeader?: string;
/** Property name before the edit, or an empty string when a new property is added. */
oldName: string;
/** Property value before the edit (including '!important' if present). */
oldValue: string;
/** Committed property name, or an empty string when the property is removed. */
newName: string;
/** Committed property value (including '!important' if present). */
newValue: string;
}
export interface StylePropertyToggle {
/** Rule header or selector containing the property (e.g. 'element.style' or '.btn:hover'). */
ruleHeader?: string;
name: string;
value: string;
/** Whether the property is now disabled. */
disabled: boolean;
}
/**
* These strings are for consumption by AI agents and do not need to be translated.
*/
function describeStylePropertyEdit({ruleHeader, oldName, oldValue, newName, newValue}: StylePropertyEdit): string {
if (!newName) {
return `Removed style property "${trimValue(oldName)}: ${trimValue(oldValue)}"${
formatRuleSuffix('from', ruleHeader)}`;
}
if (!oldName) {
return `Added style property "${trimValue(newName)}: ${trimValue(newValue)}"${formatRuleSuffix('to', ruleHeader)}`;
}
if (oldName !== newName) {
return `Changed style property from "${trimValue(oldName)}: ${trimValue(oldValue)}" to "${trimValue(newName)}: ${
trimValue(newValue)}"${formatRuleSuffix('in', ruleHeader)}`;
}
return `Changed style property "${trimValue(newName)}" from "${trimValue(oldValue)}" to "${trimValue(newValue)}"${
formatRuleSuffix('in', ruleHeader)}`;
}
function describeStylePropertyToggle({ruleHeader, name, value, disabled}: StylePropertyToggle): string {
const action = disabled ? 'Disabled' : 'Enabled';
return `${action} style property "${trimValue(name)}: ${trimValue(value)}"${formatRuleSuffix('in', ruleHeader)}`;
}
export function trackStylePropertyEdit(
tracker: ChangeTracker.ChangeTracker.ChangeTracker|undefined,
node: SDK.DOMModel.DOMNode,
edit: StylePropertyEdit,
): void {
if (!tracker?.isTracking) {
return;
}
const oldName = edit.oldName.trim();
const oldValue = edit.oldValue.trim();
const newName = edit.newName.trim();
const newValue = edit.newValue.trim();
if (oldName === newName && oldValue === newValue) {
return;
}
tracker.trackChange(
describeStylePropertyEdit({ruleHeader: edit.ruleHeader, oldName, oldValue, newName, newValue}),
buildAnchor(STYLES_PANE_VE_PATH, node),
);
}
export function trackStylePropertyToggle(
tracker: ChangeTracker.ChangeTracker.ChangeTracker|undefined,
node: SDK.DOMModel.DOMNode,
toggle: StylePropertyToggle,
): void {
if (!tracker?.isTracking) {
return;
}
tracker.trackChange(describeStylePropertyToggle(toggle), buildAnchor(STYLES_PANE_VE_PATH, node));
}
export function trackStyleRuleAdded(
tracker: ChangeTracker.ChangeTracker.ChangeTracker|undefined,
node: SDK.DOMModel.DOMNode,
selector: string,
): void {
if (!tracker?.isTracking) {
return;
}
const trimmedSelector = Platform.StringUtilities.collapseWhitespace(selector.trim());
if (!trimmedSelector) {
return;
}
tracker.trackChange(
`Added style rule "${trimValue(trimmedSelector)}"`,
buildAnchor(STYLES_PANE_VE_PATH, node),
);
}
function trackRuleHeaderChange(
tracker: ChangeTracker.ChangeTracker.ChangeTracker|undefined,
node: SDK.DOMModel.DOMNode,
headerDescription: string,
oldText: string,
newText: string,
): void {
if (!tracker?.isTracking) {
return;
}
const trimmedOld = Platform.StringUtilities.collapseWhitespace(oldText.trim());
const trimmedNew = Platform.StringUtilities.collapseWhitespace(newText.trim());
if (!trimmedOld || !trimmedNew || trimmedOld === trimmedNew) {
return;
}
tracker.trackChange(
`Changed ${headerDescription} from "${trimValue(trimmedOld)}" to "${trimValue(trimmedNew)}"`,
buildAnchor(STYLES_PANE_VE_PATH, node),
);
}
export function trackStyleSelectorChange(
tracker: ChangeTracker.ChangeTracker.ChangeTracker|undefined,
node: SDK.DOMModel.DOMNode,
oldSelector: string,
newSelector: string,
): void {
trackRuleHeaderChange(tracker, node, 'style rule selector', oldSelector, newSelector);
}
export function trackPropertyRuleNameChange(
tracker: ChangeTracker.ChangeTracker.ChangeTracker|undefined,
node: SDK.DOMModel.DOMNode,
oldName: string,
newName: string,
): void {
trackRuleHeaderChange(tracker, node, '@property rule name', oldName, newName);
}
export function trackKeyframeKeyChange(
tracker: ChangeTracker.ChangeTracker.ChangeTracker|undefined,
node: SDK.DOMModel.DOMNode,
oldKey: string,
newKey: string,
): void {
trackRuleHeaderChange(tracker, node, 'keyframe key', oldKey, newKey);
}
export type StyleQueryType = '@media'|'@container'|'@supports'|'@scope'|'@navigation';
function describeStyleQueryChange(queryType: StyleQueryType, oldQuery: string, newQuery: string): string {
if (!newQuery) {
return `Cleared ${queryType} query "${trimValue(oldQuery)}"`;
}
return `Changed ${queryType} query from "${trimValue(oldQuery)}" to "${trimValue(newQuery)}"`;
}
export function trackStyleQueryChange(
tracker: ChangeTracker.ChangeTracker.ChangeTracker|undefined,
node: SDK.DOMModel.DOMNode,
queryType: StyleQueryType,
oldQuery: string,
newQuery: string,
): void {
if (!tracker?.isTracking) {
return;
}
const trimmedOld = Platform.StringUtilities.collapseWhitespace(oldQuery.trim());
const trimmedNew = Platform.StringUtilities.collapseWhitespace(newQuery.trim());
if (trimmedOld === trimmedNew) {
return;
}
// Empty `trimmedOld` is unreachable as empty queries are not editable in the Styles pane.
if (!trimmedOld) {
return;
}
// Only `@media` and `@scope` allow empty query text in CSS.
if (!trimmedNew && queryType !== '@media' && queryType !== '@scope') {
return;
}
tracker.trackChange(
describeStyleQueryChange(queryType, trimmedOld, trimmedNew),
buildAnchor(STYLES_PANE_VE_PATH, node),
);
}