@quick-game/cli
Version:
Command line interface for rapid qg development
1,027 lines (1,026 loc) • 77.2 kB
JavaScript
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * 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 Bindings from '../../models/bindings/bindings.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';
import * as IconButton from '../../ui/components/icon_button/icon_button.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 { BezierPopoverIcon, ColorSwatchPopoverIcon, ShadowSwatchPopoverHelper, } from './ColorSwatchPopoverIcon.js';
import * as ElementsComponents from './components/components.js';
import { cssRuleValidatorsMap } from './CSSRuleValidator.js';
import { ElementsPanel } from './ElementsPanel.js';
import { StyleEditorWidget } from './StyleEditorWidget.js';
import { getCssDeclarationAsJavascriptProperty } from './StylePropertyUtils.js';
import { CSSPropertyPrompt, REGISTERED_PROPERTY_SECTION_NAME, StylesSidebarPane, StylesSidebarPropertyRenderer, } from './StylesSidebarPane.js';
const FlexboxEditor = ElementsComponents.StylePropertyEditor.FlexboxEditor;
const GridEditor = ElementsComponents.StylePropertyEditor.GridEditor;
export const activeHints = new WeakMap();
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
*/
revealInSourcesPanel: 'Reveal 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 copy all the CSS changes
*/
copyAllCSSChanges: 'Copy all CSS changes',
/**
*@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 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',
};
const str_ = i18n.i18n.registerUIStrings('panels/elements/StylePropertyTreeElement.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const parentMap = new WeakMap();
export class StylePropertyTreeElement extends UI.TreeOutline.TreeElement {
style;
matchedStylesInternal;
property;
inheritedInternal;
overloadedInternal;
parentPaneInternal;
isShorthand;
applyStyleThrottler;
newProperty;
expandedDueToFilter;
valueElement;
nameElement;
expandElement;
originalPropertyText;
hasBeenEditedIncrementally;
prompt;
lastComputedValue;
computedStyles = null;
parentsComputedStyles = null;
contextForTest;
#propertyTextFromSource;
constructor({ stylesPane, matchedStyles, property, isShorthand, inherited, overloaded, newProperty }) {
// Pass an empty title, the title gets made later in onattach.
super('', isShorthand);
this.style = property.ownerStyle;
this.matchedStylesInternal = matchedStyles;
this.property = property;
this.inheritedInternal = inherited;
this.overloadedInternal = overloaded;
this.selectable = false;
this.parentPaneInternal = stylesPane;
this.isShorthand = isShorthand;
this.applyStyleThrottler = new Common.Throttler.Throttler(0);
this.newProperty = newProperty;
if (this.newProperty) {
this.listItemElement.textContent = '';
}
this.expandedDueToFilter = false;
this.valueElement = null;
this.nameElement = null;
this.expandElement = null;
this.originalPropertyText = '';
this.hasBeenEditedIncrementally = false;
this.prompt = null;
this.lastComputedValue = null;
this.#propertyTextFromSource = property.propertyText || '';
}
matchedStyles() {
return this.matchedStylesInternal;
}
editable() {
return Boolean(this.style.styleSheetId && this.style.range);
}
inherited() {
return this.inheritedInternal;
}
overloaded() {
return this.overloadedInternal;
}
setOverloaded(x) {
if (x === this.overloadedInternal) {
return;
}
this.overloadedInternal = x;
this.updateState();
}
setComputedStyles(computedStyles) {
this.computedStyles = computedStyles;
}
setParentsComputedStyles(parentsComputedStyles) {
this.parentsComputedStyles = parentsComputedStyles;
}
get name() {
return this.property.name;
}
get value() {
return this.property.value;
}
updateFilter() {
const regex = this.parentPaneInternal.filterRegex();
const matches = regex !== null && (regex.test(this.property.name) || regex.test(this.property.value));
this.listItemElement.classList.toggle('filter-match', matches);
void this.onpopulate();
let hasMatchingChildren = false;
for (let i = 0; i < this.childCount(); ++i) {
const child = this.childAt(i);
if (!child || (child && !child.updateFilter())) {
continue;
}
hasMatchingChildren = true;
}
if (!regex) {
if (this.expandedDueToFilter) {
this.collapse();
}
this.expandedDueToFilter = false;
}
else if (hasMatchingChildren && !this.expanded) {
this.expand();
this.expandedDueToFilter = true;
}
else if (!hasMatchingChildren && this.expanded && this.expandedDueToFilter) {
this.collapse();
this.expandedDueToFilter = false;
}
return matches;
}
renderColorSwatch(text, valueChild) {
const useUserSettingFormat = this.editable();
const shiftClickMessage = i18nString(UIStrings.shiftClickToChangeColorFormat);
const tooltip = this.editable() ? i18nString(UIStrings.openColorPickerS, { PH1: shiftClickMessage }) : shiftClickMessage;
const swatch = new InlineEditor.ColorSwatch.ColorSwatch();
swatch.renderColor(text, useUserSettingFormat, tooltip);
if (!valueChild) {
valueChild = swatch.createChild('span');
const color = swatch.getColor();
valueChild.textContent =
color ? (color.getAuthoredText() ?? color.asString(swatch.getFormat() ?? undefined)) : text;
}
swatch.appendChild(valueChild);
const onColorChanged = (event) => {
const { data } = event;
swatch.firstElementChild && swatch.firstElementChild.remove();
swatch.createChild('span').textContent = data.text;
void this.applyStyleText(this.renderedPropertyText(), false);
};
swatch.addEventListener(InlineEditor.ColorSwatch.ClickEvent.eventName, () => {
Host.userMetrics.swatchActivated(2 /* Host.UserMetrics.SwatchType.Color */);
});
swatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, onColorChanged);
if (this.editable()) {
const swatchIcon = new ColorSwatchPopoverIcon(this, this.parentPaneInternal.swatchPopoverHelper(), swatch);
swatchIcon.addEventListener("colorchanged" /* ColorSwatchPopoverIconEvents.ColorChanged */, ev => {
// TODO(crbug.com/1402233): Is it really okay to dispatch an event from `Swatch` here?
// This needs consideration as current structure feels a bit different:
// There are: ColorSwatch, ColorSwatchPopoverIcon, and Spectrum
// * Our entry into the Spectrum is `ColorSwatch` and `ColorSwatch` is able to
// update the color too. (its format at least, don't know the difference)
// * ColorSwatchPopoverIcon is a helper to show/hide the Spectrum popover
// * Spectrum is the color picker
//
// My idea is: merge `ColorSwatch` and `ColorSwatchPopoverIcon`
// and emit `ColorChanged` event whenever color is changed.
// Until then, this is a hack to kind of emulate the behavior described above
// `swatch` is dispatching its own ColorChangedEvent with the changed
// color text whenever the color changes.
swatch.dispatchEvent(new InlineEditor.ColorSwatch.ColorChangedEvent(ev.data));
});
void this.addColorContrastInfo(swatchIcon);
}
return swatch;
}
processAnimationName(animationNamePropertyText) {
const animationNames = animationNamePropertyText.split(',').map(name => name.trim());
const contentChild = document.createElement('span');
for (let i = 0; i < animationNames.length; i++) {
const animationName = animationNames[i];
const swatch = new InlineEditor.LinkSwatch.LinkSwatch();
UI.UIUtils.createTextChild(swatch, animationName);
const isDefined = Boolean(this.matchedStylesInternal.keyframes().find(kf => kf.name().text === animationName));
swatch.data = {
text: animationName,
isDefined,
onLinkActivate: () => {
Host.userMetrics.swatchActivated(1 /* Host.UserMetrics.SwatchType.AnimationNameLink */);
this.parentPaneInternal.jumpToSectionBlock(`@keyframes ${animationName}`);
},
};
contentChild.appendChild(swatch);
if (i !== animationNames.length - 1) {
contentChild.appendChild(document.createTextNode(', '));
}
}
return contentChild;
}
processAnimation(animationPropertyValue) {
const animationNameProperty = this.property.getLonghandProperties().find(longhand => longhand.name === 'animation-name');
if (!animationNameProperty) {
return document.createTextNode(animationPropertyValue);
}
const animationNames = animationNameProperty.value.split(',').map(name => name.trim());
const cssAnimationModel = InlineEditor.CSSAnimationModel.CSSAnimationModel.parse(animationPropertyValue, animationNames);
const contentChild = document.createElement('span');
for (let i = 0; i < cssAnimationModel.parts.length; i++) {
const part = cssAnimationModel.parts[i];
switch (part.type) {
case "T" /* InlineEditor.CSSAnimationModel.PartType.Text */:
contentChild.appendChild(document.createTextNode(part.value));
break;
case "EF" /* InlineEditor.CSSAnimationModel.PartType.EasingFunction */:
contentChild.appendChild(this.processBezier(part.value));
break;
case "AN" /* InlineEditor.CSSAnimationModel.PartType.AnimationName */:
contentChild.appendChild(this.processAnimationName(part.value));
break;
case "V" /* InlineEditor.CSSAnimationModel.PartType.Variable */:
contentChild.appendChild(this.processVar(part.value));
break;
}
if (cssAnimationModel.parts[i + 1]?.value !== ',' && i !== cssAnimationModel.parts.length - 1) {
contentChild.appendChild(document.createTextNode(' '));
}
}
return contentChild;
}
processPositionFallback(propertyText) {
const contentChild = document.createElement('span');
const swatch = new InlineEditor.LinkSwatch.LinkSwatch();
UI.UIUtils.createTextChild(swatch, propertyText);
const isDefined = Boolean(this.matchedStylesInternal.positionFallbackRules().find(pf => pf.name().text === propertyText));
swatch.data = {
text: propertyText,
isDefined,
onLinkActivate: () => {
Host.userMetrics.swatchActivated(9 /* Host.UserMetrics.SwatchType.PositionFallbackLink */);
this.parentPaneInternal.jumpToSectionBlock(`@position-fallback ${propertyText}`);
},
};
contentChild.appendChild(swatch);
return contentChild;
}
processColor(text, valueChild) {
return this.renderColorSwatch(text, valueChild);
}
processColorMix(text) {
let colorMixText = text;
let interpolationMethodResolvedCorrectly = false;
const paramColorValues = [];
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse(text);
if (!colorMixModel) {
return document.createTextNode(text);
}
const handleInterpolationMethod = (interpolationMethod) => {
const matches = TextUtils.TextUtils.Utils.splitStringByRegexes(interpolationMethod, [SDK.CSSMetadata.VariableRegex]);
for (const match of matches) {
if (match.regexIndex === 0) {
const computedSingleValue = this.matchedStylesInternal.computeSingleVariableValue(this.style, match.value);
if (!computedSingleValue || !computedSingleValue.computedValue) {
return;
}
colorMixText = colorMixText.replace(match.value, computedSingleValue.computedValue);
const varSwatch = this.processVar(match.value);
contentChild.appendChild(varSwatch);
}
else {
contentChild.appendChild(document.createTextNode(match.value));
}
}
interpolationMethodResolvedCorrectly = true;
return;
};
const handleValue = (value, onChange) => {
// Parameter is a CSS variable
if (value.match(SDK.CSSMetadata.VariableRegex)) {
const computedSingleValue = this.matchedStylesInternal.computeSingleVariableValue(this.style, value);
// The variable is not defined or it is not a color
if (!computedSingleValue || !computedSingleValue.computedValue ||
!Common.Color.parse(computedSingleValue.computedValue)) {
return;
}
const { computedValue } = computedSingleValue;
// Update `var` reference in the color mix text with the variable's
// computed value since the same variable is not defined in DevTools
// reference to that in the CSS will result in undefined color.
colorMixText = colorMixText.replace(value, computedValue);
const varSwatch = this.processVar(value);
if (varSwatch instanceof InlineEditor.ColorSwatch.ColorSwatch) {
varSwatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, (ev) => {
onChange(ev.data.text);
});
}
contentChild.appendChild(varSwatch);
paramColorValues.push(computedSingleValue.computedValue);
return;
}
// Parameter is specified as an actual color (i.e. #000)
if (value.match(Common.Color.Regex)) {
const colorSwatch = this.processColor(value);
if (colorSwatch instanceof InlineEditor.ColorSwatch.ColorSwatch) {
colorSwatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, (ev) => {
onChange(ev.data.text);
});
}
contentChild.appendChild(colorSwatch);
paramColorValues.push(value);
}
};
const handleParam = (paramParts, onChange) => {
for (let i = 0; i < paramParts.length; i++) {
const part = paramParts[i];
if (part.name === "V" /* InlineEditor.ColorMixModel.PartName.Value */) {
handleValue(part.value, onChange);
}
else {
contentChild.appendChild(document.createTextNode(part.value));
}
if (i !== paramParts.length - 1) {
contentChild.appendChild(document.createTextNode(' '));
}
}
};
const [interpolationMethod, firstParam, secondParam] = colorMixModel.parts;
const swatch = new InlineEditor.ColorMixSwatch.ColorMixSwatch();
const contentChild = document.createElement('span');
contentChild.appendChild(document.createTextNode('color-mix('));
handleInterpolationMethod(interpolationMethod.value);
contentChild.appendChild(document.createTextNode(', '));
handleParam(firstParam.value, (color) => {
swatch.setFirstColor(color);
});
contentChild.appendChild(document.createTextNode(', '));
handleParam(secondParam.value, (color) => {
swatch.setSecondColor(color);
});
contentChild.appendChild(document.createTextNode(')'));
if (paramColorValues.length !== 2 || !interpolationMethodResolvedCorrectly) {
return document.createTextNode(text);
}
swatch.appendChild(contentChild);
swatch.setFirstColor(paramColorValues[0]);
swatch.setSecondColor(paramColorValues[1]);
swatch.setColorMixText(colorMixText);
return swatch;
}
processVar(text) {
const computedSingleValue = this.matchedStylesInternal.computeSingleVariableValue(this.style, text);
if (!computedSingleValue) {
return document.createTextNode(text);
}
const { computedValue, fromFallback } = computedSingleValue;
const varSwatch = new InlineEditor.LinkSwatch.CSSVarSwatch();
UI.UIUtils.createTextChild(varSwatch, text);
varSwatch.data = {
text,
computedValue,
fromFallback,
onLinkActivate: this.handleVarDefinitionActivate.bind(this),
};
if (varSwatch.link?.linkElement) {
const { textContent } = varSwatch.link.linkElement;
this.parentPaneInternal.addPopover(varSwatch.link, () => textContent ? this.#getVariablePopoverContents(textContent, computedValue) : undefined);
}
if (!computedValue || !Common.Color.parse(computedValue)) {
return varSwatch;
}
return this.processColor(computedValue, varSwatch);
}
handleVarDefinitionActivate(variableName) {
Host.userMetrics.actionTaken(Host.UserMetrics.Action.CustomPropertyLinkClicked);
Host.userMetrics.swatchActivated(0 /* Host.UserMetrics.SwatchType.VarLink */);
this.parentPaneInternal.jumpToProperty(variableName) ||
this.parentPaneInternal.jumpToProperty('initial-value', variableName, REGISTERED_PROPERTY_SECTION_NAME);
}
async addColorContrastInfo(swatchIcon) {
if (this.property.name !== 'color' || !this.parentPaneInternal.cssModel() || !this.node()) {
return;
}
const cssModel = this.parentPaneInternal.cssModel();
const node = this.node();
if (cssModel && node && typeof node.id !== 'undefined') {
const contrastInfo = new ColorPicker.ContrastInfo.ContrastInfo(await cssModel.getBackgroundColors(node.id));
swatchIcon.setContrastInfo(contrastInfo);
}
}
renderedPropertyText() {
if (!this.nameElement || !this.valueElement) {
return '';
}
return this.nameElement.textContent + ': ' + this.valueElement.textContent;
}
processBezier(text) {
if (!this.editable() || !InlineEditor.AnimationTimingModel.AnimationTimingModel.parse(text)) {
return document.createTextNode(text);
}
const swatchPopoverHelper = this.parentPaneInternal.swatchPopoverHelper();
const swatch = InlineEditor.Swatches.BezierSwatch.create();
swatch.iconElement().addEventListener('click', () => {
Host.userMetrics.swatchActivated(3 /* Host.UserMetrics.SwatchType.AnimationTiming */);
});
swatch.setBezierText(text);
new BezierPopoverIcon({ treeElement: this, swatchPopoverHelper, swatch });
return swatch;
}
processFont(text) {
const section = this.section();
if (section) {
section.registerFontProperty(this);
}
return document.createTextNode(text);
}
processShadow(propertyValue, propertyName) {
if (!this.editable()) {
return document.createTextNode(propertyValue);
}
let shadows;
if (propertyName === 'text-shadow') {
shadows = InlineEditor.CSSShadowModel.CSSShadowModel.parseTextShadow(propertyValue);
}
else {
shadows = InlineEditor.CSSShadowModel.CSSShadowModel.parseBoxShadow(propertyValue);
}
if (!shadows.length) {
return document.createTextNode(propertyValue);
}
const container = document.createDocumentFragment();
const swatchPopoverHelper = this.parentPaneInternal.swatchPopoverHelper();
for (let i = 0; i < shadows.length; i++) {
if (i !== 0) {
container.appendChild(document.createTextNode(', '));
} // Add back commas and spaces between each shadow.
// TODO(flandy): editing the property value should use the original value with all spaces.
const cssShadowSwatch = InlineEditor.Swatches.CSSShadowSwatch.create();
cssShadowSwatch.setCSSShadow(shadows[i]);
cssShadowSwatch.iconElement().addEventListener('click', () => {
Host.userMetrics.swatchActivated(4 /* Host.UserMetrics.SwatchType.Shadow */);
});
new ShadowSwatchPopoverHelper(this, swatchPopoverHelper, cssShadowSwatch);
const colorSwatch = cssShadowSwatch.colorSwatch();
if (colorSwatch) {
colorSwatch.addEventListener(InlineEditor.ColorSwatch.ClickEvent.eventName, () => {
Host.userMetrics.swatchActivated(2 /* Host.UserMetrics.SwatchType.Color */);
});
const swatchIcon = new ColorSwatchPopoverIcon(this, swatchPopoverHelper, colorSwatch);
swatchIcon.addEventListener("colorchanged" /* ColorSwatchPopoverIconEvents.ColorChanged */, ev => {
// TODO(crbug.com/1402233): Is it really okay to dispatch an event from `Swatch` here?
colorSwatch.dispatchEvent(new InlineEditor.ColorSwatch.ColorChangedEvent(ev.data));
});
colorSwatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, () => {
void this.applyStyleText(this.renderedPropertyText(), false);
});
}
container.appendChild(cssShadowSwatch);
}
return container;
}
processGrid(propertyValue, _propertyName) {
const splitResult = TextUtils.TextUtils.Utils.splitStringByRegexes(propertyValue, [SDK.CSSMetadata.GridAreaRowRegex]);
if (splitResult.length <= 1) {
return document.createTextNode(propertyValue);
}
const indent = Common.Settings.Settings.instance().moduleSetting('textEditorIndent').get();
const container = document.createDocumentFragment();
for (const result of splitResult) {
const value = result.value.trim();
const content = UI.Fragment.html `<br /><span class='styles-clipboard-only'>${indent.repeat(2)}</span>${value}`;
container.appendChild(content);
}
return container;
}
processAngle(angleText) {
if (!this.editable()) {
return document.createTextNode(angleText);
}
const cssAngle = new InlineEditor.CSSAngle.CSSAngle();
const valueElement = document.createElement('span');
valueElement.textContent = angleText;
const computedPropertyValue = this.matchedStylesInternal.computeValue(this.property.ownerStyle, this.property.value) || '';
cssAngle.data = {
propertyName: this.property.name,
propertyValue: computedPropertyValue,
angleText,
containingPane: this.parentPaneInternal.element.enclosingNodeOrSelfWithClass('style-panes-wrapper'),
};
cssAngle.append(valueElement);
const popoverToggled = (event) => {
const section = this.section();
if (!section) {
return;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data } = event;
if (data.open) {
this.parentPaneInternal.hideAllPopovers();
this.parentPaneInternal.activeCSSAngle = cssAngle;
Host.userMetrics.swatchActivated(7 /* Host.UserMetrics.SwatchType.Angle */);
}
section.element.classList.toggle('has-open-popover', data.open);
this.parentPaneInternal.setEditingStyle(data.open);
};
const valueChanged = async (event) => {
const { data } = event;
valueElement.textContent = data.value;
await this.applyStyleText(this.renderedPropertyText(), false);
const computedPropertyValue = this.matchedStylesInternal.computeValue(this.property.ownerStyle, this.property.value) || '';
cssAngle.updateProperty(this.property.name, computedPropertyValue);
};
const unitChanged = async (event) => {
const { data } = event;
valueElement.textContent = data.value;
};
cssAngle.addEventListener('popovertoggled', popoverToggled);
cssAngle.addEventListener('valuechanged', valueChanged);
cssAngle.addEventListener('unitchanged', unitChanged);
return cssAngle;
}
processLength(lengthText) {
if (!this.editable()) {
return document.createTextNode(lengthText);
}
const cssLength = new InlineEditor.CSSLength.CSSLength();
const valueElement = document.createElement('span');
valueElement.textContent = lengthText;
cssLength.data = {
lengthText,
overloaded: this.overloadedInternal,
};
cssLength.append(valueElement);
const onValueChanged = (event) => {
const { data } = event;
valueElement.textContent = data.value;
this.parentPaneInternal.setEditingStyle(true);
void this.applyStyleText(this.renderedPropertyText(), false);
};
const onDraggingFinished = () => {
this.parentPaneInternal.setEditingStyle(false);
};
cssLength.addEventListener('valuechanged', onValueChanged);
cssLength.addEventListener('draggingfinished', onDraggingFinished);
return cssLength;
}
updateState() {
if (!this.listItemElement) {
return;
}
if (this.style.isPropertyImplicit(this.name)) {
this.listItemElement.classList.add('implicit');
}
else {
this.listItemElement.classList.remove('implicit');
}
const hasIgnorableError = !this.property.parsedOk && StylesSidebarPane.ignoreErrorsForProperty(this.property);
if (hasIgnorableError) {
this.listItemElement.classList.add('has-ignorable-error');
}
else {
this.listItemElement.classList.remove('has-ignorable-error');
}
if (this.inherited()) {
this.listItemElement.classList.add('inherited');
}
else {
this.listItemElement.classList.remove('inherited');
}
if (this.overloaded()) {
this.listItemElement.classList.add('overloaded');
}
else {
this.listItemElement.classList.remove('overloaded');
}
if (this.property.disabled) {
this.listItemElement.classList.add('disabled');
}
else {
this.listItemElement.classList.remove('disabled');
}
this.listItemElement.classList.toggle('changed', this.isPropertyChanged(this.property));
}
node() {
return this.parentPaneInternal.node();
}
parentPane() {
return this.parentPaneInternal;
}
section() {
if (!this.treeOutline) {
return null;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return this.treeOutline.section;
}
updatePane() {
const section = this.section();
if (section) {
section.refreshUpdate(this);
}
}
async toggleDisabled(disabled) {
const oldStyleRange = this.style.range;
if (!oldStyleRange) {
return;
}
this.parentPaneInternal.setUserOperation(true);
const success = await this.property.setDisabled(disabled);
this.parentPaneInternal.setUserOperation(false);
if (!success) {
return;
}
this.matchedStylesInternal.resetActiveProperties();
this.updatePane();
this.styleTextAppliedForTest();
}
isPropertyChanged(property) {
if (!Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.STYLES_PANE_CSS_CHANGES)) {
return false;
}
// Check local cache first, then check against diffs from the workspace.
return this.#propertyTextFromSource !== property.propertyText || this.parentPane().isPropertyChanged(property);
}
async onpopulate() {
// Only populate once and if this property is a shorthand.
if (this.childCount() || !this.isShorthand) {
return;
}
const longhandProperties = this.property.getLonghandProperties();
const leadingProperties = this.style.leadingProperties();
for (const property of longhandProperties) {
const name = property.name;
let inherited = false;
let overloaded = false;
const section = this.section();
if (section) {
inherited = section.isPropertyInherited(name);
overloaded =
this.matchedStylesInternal.propertyState(property) === SDK.CSSMatchedStyles.PropertyState.Overloaded;
}
const leadingProperty = leadingProperties.find(property => property.name === name && property.activeInStyle());
if (leadingProperty) {
overloaded = true;
}
const item = new StylePropertyTreeElement({
stylesPane: this.parentPaneInternal,
matchedStyles: this.matchedStylesInternal,
property,
isShorthand: false,
inherited,
overloaded,
newProperty: false,
});
item.setComputedStyles(this.computedStyles);
item.setParentsComputedStyles(this.parentsComputedStyles);
this.appendChild(item);
}
}
onattach() {
this.updateTitle();
this.listItemElement.addEventListener('mousedown', event => {
if (event.button === 0) {
parentMap.set(this.parentPaneInternal, this);
}
}, false);
this.listItemElement.addEventListener('mouseup', this.mouseUp.bind(this));
this.listItemElement.addEventListener('click', event => {
if (!event.target) {
return;
}
const node = event.target;
if (!node.hasSelection() && event.target !== this.listItemElement) {
event.consume(true);
}
});
// Copy context menu.
this.listItemElement.addEventListener('contextmenu', this.handleCopyContextMenuEvent.bind(this));
}
onexpand() {
this.updateExpandElement();
}
oncollapse() {
this.updateExpandElement();
}
updateExpandElement() {
if (!this.expandElement) {
return;
}
if (this.expanded) {
this.expandElement.setIconType('triangle-down');
}
else {
this.expandElement.setIconType('triangle-right');
}
}
#getRegisteredPropertyDetails(variableName) {
const registration = this.matchedStyles().getRegisteredProperty(variableName);
const goToDefinition = () => this.parentPaneInternal.jumpToSection(variableName, REGISTERED_PROPERTY_SECTION_NAME);
return registration ? { registration, goToDefinition } : undefined;
}
#getVariablePopoverContents(variableName, computedValue) {
const registrationDetails = this.#getRegisteredPropertyDetails(variableName);
if (!registrationDetails && !computedValue) {
return undefined;
}
return new ElementsComponents.CSSVariableValueView.CSSVariableValueView(computedValue ?? '', registrationDetails);
}
updateTitleIfComputedValueChanged() {
const computedValue = this.matchedStylesInternal.computeValue(this.property.ownerStyle, this.property.value);
if (computedValue === this.lastComputedValue) {
return;
}
this.lastComputedValue = computedValue;
this.innerUpdateTitle();
}
updateTitle() {
this.lastComputedValue = this.matchedStylesInternal.computeValue(this.property.ownerStyle, this.property.value);
this.innerUpdateTitle();
}
innerUpdateTitle() {
this.updateState();
if (this.isExpandable()) {
this.expandElement = UI.Icon.Icon.create('triangle-right', 'expand-icon');
}
else {
this.expandElement = null;
}
const propertyRenderer = new StylesSidebarPropertyRenderer(this.style.parentRule, this.node(), this.name, this.value);
if (this.property.parsedOk) {
propertyRenderer.setVarHandler(this.processVar.bind(this));
propertyRenderer.setAnimationNameHandler(this.processAnimationName.bind(this));
propertyRenderer.setAnimationHandler(this.processAnimation.bind(this));
propertyRenderer.setColorHandler(this.processColor.bind(this));
propertyRenderer.setColorMixHandler(this.processColorMix.bind(this));
propertyRenderer.setBezierHandler(this.processBezier.bind(this));
propertyRenderer.setFontHandler(this.processFont.bind(this));
propertyRenderer.setShadowHandler(this.processShadow.bind(this));
propertyRenderer.setGridHandler(this.processGrid.bind(this));
propertyRenderer.setAngleHandler(this.processAngle.bind(this));
propertyRenderer.setLengthHandler(this.processLength.bind(this));
propertyRenderer.setPositionFallbackHandler(this.processPositionFallback.bind(this));
}
this.listItemElement.removeChildren();
this.nameElement = propertyRenderer.renderName();
if (this.property.name.startsWith('--') && this.nameElement) {
this.parentPaneInternal.addPopover(this.nameElement, () => this.#getVariablePopoverContents(this.property.name, this.matchedStylesInternal.computeCSSVariable(this.style, this.property.name)));
}
this.valueElement = propertyRenderer.renderValue();
if (!this.treeOutline) {
return;
}
const indent = Common.Settings.Settings.instance().moduleSetting('textEditorIndent').get();
UI.UIUtils.createTextChild(this.listItemElement.createChild('span', 'styles-clipboard-only'), indent + (this.property.disabled ? '/* ' : ''));
if (this.nameElement) {
this.listItemElement.appendChild(this.nameElement);
}
if (this.valueElement) {
const lineBreakValue = this.valueElement.firstElementChild && this.valueElement.firstElementChild.tagName === 'BR';
const separator = lineBreakValue ? ':' : ': ';
this.listItemElement.createChild('span', 'styles-name-value-separator').textContent = separator;
if (this.expandElement) {
this.listItemElement.appendChild(this.expandElement);
}
this.listItemElement.appendChild(this.valueElement);
const semicolon = this.listItemElement.createChild('span', 'styles-semicolon');
semicolon.textContent = ';';
semicolon.onmouseup = this.mouseUp.bind(this);
if (this.property.disabled) {
UI.UIUtils.createTextChild(this.listItemElement.createChild('span', 'styles-clipboard-only'), ' */');
}
}
const section = this.section();
if (this.valueElement && section && section.editable && this.property.name === 'display') {
const propertyValue = this.property.trimmedValueWithoutImportant();
const isFlex = propertyValue === 'flex' || propertyValue === 'inline-flex';
const isGrid = propertyValue === 'grid' || propertyValue === 'inline-grid';
if (isFlex || isGrid) {
const key = `${section.getSectionIdx()}_${section.nextEditorTriggerButtonIdx}`;
const button = StyleEditorWidget.createTriggerButton(this.parentPaneInternal, section, isFlex ? FlexboxEditor : GridEditor, isFlex ? i18nString(UIStrings.flexboxEditorButton) : i18nString(UIStrings.gridEditorButton), key);
section.nextEditorTriggerButtonIdx++;
button.addEventListener('click', () => {
Host.userMetrics.swatchActivated(isFlex ? 6 /* Host.UserMetrics.SwatchType.Flex */ : 5 /* Host.UserMetrics.SwatchType.Grid */);
});
this.listItemElement.appendChild(button);
const helper = this.parentPaneInternal.swatchPopoverHelper();
if (helper.isShowing(StyleEditorWidget.instance()) && StyleEditorWidget.instance().getTriggerKey() === key) {
helper.setAnchorElement(button);
}
}
}
if (this.property.parsedOk) {
this.updateAuthoringHint();
}
else {
// Avoid having longhands under an invalid shorthand.
this.listItemElement.classList.add('not-parsed-ok');
const registrationDetails = this.#getRegisteredPropertyDetails(this.property.name);
const tooltip = registrationDetails ?
new ElementsComponents.CSSVariableValueView.CSSVariableParserError(registrationDetails) :
null;
// Add a separate exclamation mark IMG element with a tooltip.
this.listItemElement.insertBefore(this.parentPaneInternal.createExclamationMark(this.property, tooltip), this.listItemElement.firstChild);
// When the property is valid but the property value is invalid,
// add line-through only to the property value.
const invalidPropertyValue = SDK.CSSMetadata.cssMetadata().isCSSPropertyName(this.property.name);
if (invalidPropertyValue) {
this.listItemElement.classList.add('invalid-property-value');
}
}
if (!this.property.activeInStyle()) {
this.listItemElement.classList.add('inactive');
}
this.updateFilter();
if (this.property.parsedOk && this.section() && this.parent && this.parent.root) {
const enabledCheckboxElement = document.createElement('input');
enabledCheckboxElement.className = 'enabled-button';
enabledCheckboxElement.type = 'checkbox';
enabledCheckboxElement.checked = !this.property.disabled;
enabledCheckboxElement.addEventListener('mousedown', event => event.consume(), false);
enabledCheckboxElement.addEventListener('click', event => {
void this.toggleDisabled(!this.property.disabled);
event.consume();
}, false);
if (this.nameElement && this.valueElement) {
UI.ARIAUtils.setLabel(enabledCheckboxElement, `${this.nameElement.textContent} ${this.valueElement.textContent}`);
}
const copyIcon = UI.Icon.Icon.create('copy', 'copy');
UI.Tooltip.Tooltip.install(copyIcon, i18nString(UIStrings.copyDeclaration));
copyIcon.addEventListener('click', () => {
const propertyText = `${this.property.name}: ${this.property.value};`;
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(propertyText);
Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.DeclarationViaChangedLine);
});
this.listItemElement.append(copyIcon);
this.listItemElement.insertBefore(enabledCheckboxElement, this.listItemElement.firstChild);
}
}
updateAuthoringHint() {
this.listItemElement.classList.remove('inactive-property');
const existingElement = this.listItemElement.querySelector('.hint');
if (existingElement) {
activeHints.delete(existingElement);
existingElement?.closest('.hint-wrapper')?.remove();
}
const propertyName = this.property.name;
if (!cssRuleValidatorsMap.has(propertyName)) {
return;
}
// Different rules apply to SVG nodes altogether. We currently don't have SVG-specific hints.
if (this.node()?.isSVGNode()) {
return;
}
const cssModel = this.parentPaneInternal.cssModel();
const fontFaces = cssModel?.fontFaces() || [];
const localName = this.node()?.localName();
for (const validator of cssRuleValidatorsMap.get(propertyName) || []) {
const hint = validator.getHint(propertyName, this.computedStyles || undefined, this.parentsComputedStyles || undefined, localName?.toLowerCase(), fontFaces);
if (hint) {
Host.userMetrics.cssHintShown(validator.getMetricType());
const wrapper = document.createElement('span');
wrapper.classList.add('hint-wrapper');
const hintIcon = new IconButton.Icon.Icon();
hintIcon.data = { iconName: 'info', color: 'var(--icon-default)', width: '14px', height: '14px' };
hintIcon.classList.add('hint');
wrapper.append(hintIcon);
activeHints.set(hintIcon, hint);
this.listItemElement.append(wrapper);
this.listItemElement.classList.add('inactive-property');
break;
}
}
}
mouseUp(event) {
const activeTreeElement = parentMap.get(this.parentPaneInternal);
parentMap.delete(this.parentPaneInternal);
if (!activeTreeElement) {
return;
}
if (this.listItemElement.hasSelection()) {
return;
}
if (UI.UIUtils.isBeingEdited(event.target)) {
return;
}
event.consume(true);
if (event.target === this.listItemElement) {
return;
}
const section = this.section();
if (UI.KeyboardShortcut.KeyboardShortcut.eventHasCtrlEquivalentKey(event) && section && section.navigable) {
this.navigateToSource(event.target);
return;
}
this.startEditing(event.target);
}
handleContextMenuEvent(context, event) {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
if (this.property.parsedOk && this.section() && this.parent && this.parent.root) {
const sectionIndex = this.parentPaneInternal.focusedSectionIndex();
contextMenu.defaultSection().appendCheckboxItem(i18nString(UIStrings.togglePropertyAndContinueEditing), async () => {
if (this.treeOutline) {
const propertyIndex = this.treeOutline.rootElement().indexOfChild(this);
// order matters here: this.editingCancelled may invalidate this.treeOutline.
this.editingCancelled(null, context);
await this.toggleDisabled(!this.property.disabled);
event.consume();
this.parentPaneInternal.continueEditingElement(sectionIndex, propertyIndex);
}
}, !this.property.disabled);
}
const revealCallback = this.navigateToSource.bind(this);
contextMenu.defaultSection().appendItem(i18nString(UIStrings.revealInSourcesPanel), revealCallback);
void contextMenu.show();
}
handleCopyContextMenuEvent(event) {
const target = event.target;
if (!target) {
return;
}
const contextMenu = this.createCopyContextMenu(event);
void contextMenu.show();
}
createCopyContextMenu(event) {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.headerSection().appendItem(i18nString(UIStrings.copyDeclaration), () => {
const propertyText = `${this.property.name}: ${this.property.value};`;
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(propertyText);
Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.DeclarationViaContextMenu);
});
contextMenu.headerSection().appendItem(i18nString(UIStrings.copyProperty), () => {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(this.property.name);
Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.PropertyViaContextMenu);
});
contextMenu.headerSection().appendItem(i18nString(UIStrings.copyValue), () => {
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(this.property.value);
Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.ValueViaContextMenu);
});
contextMenu.headerSection().appendItem(i18nString(UIStrings.copyRule), () => {
const section = this.section();
const ruleText = StylesSidebarPane.formatLeadingProperties(section).ruleText;
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(ruleText);
Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.RuleViaContextMenu);
});
contextMenu.headerSection().appendItem(i18nString(UIStrings.copyCssDeclarationAsJs), this.copyCssDeclarationAsJs.bind(this));
contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyAllDeclarations), () => {
const section = this.section();
const allDeclarationText = StylesSidebarPane.formatLeadingProperties(section).allDeclarationText;
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(allDeclarationText);
Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.AllDeclarationsViaContextMenu);
});
contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyAllCssDeclarationsAsJs), this.copyAllCssDeclarationAsJs.bind(this));
// TODO(changhaohan): conditionally add this item only when there are changes to copy
contextMenu.defaultSection().appendItem(i18nString(UIStrings.copyAllCSSChanges), async () => {
const allChanges = await this.parentPane().getFormattedChanges();
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(allChanges);
Host.userMetrics.styleTextCopied(Host.UserMetrics.StyleTextCopied.AllChangesViaStylesPane);
});