UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

1,027 lines (1,026 loc) 77.2 kB
// 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); });