UNPKG

chrome-devtools-frontend

Version:
694 lines (612 loc) 23.9 kB
// Copyright 2022 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 Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as SDK from '../../core/sdk/sdk.js'; import { buildPropertyDefinitionText, buildPropertyName, buildPropertyValue, isFlexContainer, isGridContainer, isInlineElement, isMulticolContainer, isPossiblyReplacedElement, } from './CSSRuleValidatorHelper.js'; const UIStrings = { /** *@description The message shown in the Style pane when the user hovers over a property that has no effect due to some other property. *@example {flex-wrap: nowrap} REASON_PROPERTY_DECLARATION_CODE *@example {align-content} AFFECTED_PROPERTY_DECLARATION_CODE */ ruleViolatedBySameElementRuleReason: 'The {REASON_PROPERTY_DECLARATION_CODE} property prevents {AFFECTED_PROPERTY_DECLARATION_CODE} from having an effect.', /** *@description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to some other property. *@example {flex-wrap} PROPERTY_NAME @example {nowrap} PROPERTY_VALUE */ ruleViolatedBySameElementRuleFix: 'Try setting {PROPERTY_NAME} to something other than {PROPERTY_VALUE}.', /** *@description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to the current property value. *@example {display: block} EXISTING_PROPERTY_DECLARATION *@example {display: flex} TARGET_PROPERTY_DECLARATION */ ruleViolatedBySameElementRuleChangeSuggestion: 'Try setting the {EXISTING_PROPERTY_DECLARATION} property to {TARGET_PROPERTY_DECLARATION}.', /** *@description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to properties of the parent element. *@example {display: block} REASON_PROPERTY_DECLARATION_CODE *@example {flex} AFFECTED_PROPERTY_DECLARATION_CODE */ ruleViolatedByParentElementRuleReason: 'The {REASON_PROPERTY_DECLARATION_CODE} property on the parent element prevents {AFFECTED_PROPERTY_DECLARATION_CODE} from having an effect.', /** *@description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to the properties of the parent element. *@example {display: block} EXISTING_PARENT_ELEMENT_RULE *@example {display: flex} TARGET_PARENT_ELEMENT_RULE */ ruleViolatedByParentElementRuleFix: 'Try setting the {EXISTING_PARENT_ELEMENT_RULE} property on the parent to {TARGET_PARENT_ELEMENT_RULE}.', /** *@description The warning text shown in Elements panel when font-variation-settings don't match allowed values *@example {wdth} PH1 *@example {100} PH2 *@example {10} PH3 *@example {20} PH4 *@example {Arial} PH5 */ fontVariationSettingsWarning: 'Value for setting “{PH1}” {PH2} is outside the supported range [{PH3}, {PH4}] for font-family “{PH5}”.', }; const str_ = i18n.i18n.registerUIStrings('panels/elements/CSSRuleValidator.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export const enum HintType { INACTIVE_PROPERTY = 'ruleValidation', DEPRECATED_PROPERTY = 'deprecatedProperty', } export class Hint { readonly #hintMessage: string; readonly #possibleFixMessage: string|null; readonly #learnMoreLink: string|undefined; constructor(hintMessage: string, possibleFixMessage: string|null, learnMoreLink?: string) { this.#hintMessage = hintMessage; this.#possibleFixMessage = possibleFixMessage; this.#learnMoreLink = learnMoreLink; } getMessage(): string { return this.#hintMessage; } getPossibleFixMessage(): string|null { return this.#possibleFixMessage; } getLearnMoreLink(): string|undefined { return this.#learnMoreLink; } } export abstract class CSSRuleValidator { getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.Other; } readonly #affectedProperties: string[]; constructor(affectedProperties: string[]) { this.#affectedProperties = affectedProperties; } getApplicableProperties(): string[] { return this.#affectedProperties; } abstract getHint( propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>, nodeName?: string, fontFaces?: Array<SDK.CSSFontFace.CSSFontFace>): Hint|undefined; } export class AlignContentValidator extends CSSRuleValidator { constructor() { super(['align-content']); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.AlignContent; } getHint(_propertyName: string, computedStyles?: Map<string, string>): Hint|undefined { if (!computedStyles) { return; } if (!isFlexContainer(computedStyles)) { return; } if (computedStyles.get('flex-wrap') !== 'nowrap') { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('flex-wrap', 'nowrap'); const affectedPropertyDeclarationCode = buildPropertyName('align-content'); return new Hint( i18nString(UIStrings.ruleViolatedBySameElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedBySameElementRuleFix, { PROPERTY_NAME: buildPropertyName('flex-wrap'), PROPERTY_VALUE: buildPropertyValue('nowrap'), }), ); } } export class FlexItemValidator extends CSSRuleValidator { constructor() { super(['flex', 'flex-basis', 'flex-grow', 'flex-shrink']); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.FlexItem; } getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint |undefined { if (!parentComputedStyles) { return; } if (isFlexContainer(parentComputedStyles)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('display', parentComputedStyles?.get('display')); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); const targetParentPropertyDeclaration = buildPropertyDefinitionText('display', 'flex'); return new Hint( i18nString(UIStrings.ruleViolatedByParentElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedByParentElementRuleFix, { 'EXISTING_PARENT_ELEMENT_RULE': reasonPropertyDeclaration, 'TARGET_PARENT_ELEMENT_RULE': targetParentPropertyDeclaration, }), ); } } export class FlexContainerValidator extends CSSRuleValidator { constructor() { super(['flex-direction', 'flex-flow', 'flex-wrap']); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.FlexContainer; } getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined { if (!computedStyles) { return; } if (isFlexContainer(computedStyles)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display')); const targetRuleCode = buildPropertyDefinitionText('display', 'flex'); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedBySameElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedBySameElementRuleChangeSuggestion, { 'EXISTING_PROPERTY_DECLARATION': reasonPropertyDeclaration, 'TARGET_PROPERTY_DECLARATION': targetRuleCode, }), ); } } export class GridContainerValidator extends CSSRuleValidator { constructor() { super([ 'grid', 'grid-auto-columns', 'grid-auto-flow', 'grid-auto-rows', 'grid-template', 'grid-template-areas', 'grid-template-columns', 'grid-template-rows', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.GridContainer; } getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined { if (isGridContainer(computedStyles)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display')); const targetRuleCode = buildPropertyDefinitionText('display', 'grid'); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedBySameElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedBySameElementRuleChangeSuggestion, { 'EXISTING_PROPERTY_DECLARATION': reasonPropertyDeclaration, 'TARGET_PROPERTY_DECLARATION': targetRuleCode, }), ); } } export class GridItemValidator extends CSSRuleValidator { constructor() { super([ 'grid-area', 'grid-column', 'grid-row', 'grid-row-end', 'grid-row-start', // At the time of writing (November 2022), `justify-self` is only in effect in grid layout. // There are no other browsers that support `justify-self` in other layouts. // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Alignment/Box_Alignment_In_Block_Abspos_Tables // TODO: move `justify-self` to other validator or change pop-over text if Chrome supports CSS Align in other layouts. 'justify-self', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.GridItem; } getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint |undefined { if (!parentComputedStyles) { return; } if (isGridContainer(parentComputedStyles)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('display', parentComputedStyles?.get('display')); const targetParentPropertyDeclaration = buildPropertyDefinitionText('display', 'grid'); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedByParentElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedByParentElementRuleFix, { 'EXISTING_PARENT_ELEMENT_RULE': reasonPropertyDeclaration, 'TARGET_PARENT_ELEMENT_RULE': targetParentPropertyDeclaration, }), ); } } export class FlexOrGridItemValidator extends CSSRuleValidator { constructor() { super([ 'place-self', 'align-self', 'order', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.FlexOrGridItem; } getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint |undefined { if (!parentComputedStyles) { return; } if (isFlexContainer(parentComputedStyles) || isGridContainer(parentComputedStyles)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('display', parentComputedStyles?.get('display')); const targetParentPropertyDeclaration = `${buildPropertyDefinitionText('display', 'flex')} or ${buildPropertyDefinitionText('display', 'grid')}`; const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedByParentElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedByParentElementRuleFix, { 'EXISTING_PARENT_ELEMENT_RULE': reasonPropertyDeclaration, 'TARGET_PARENT_ELEMENT_RULE': targetParentPropertyDeclaration, }), ); } } export class FlexGridValidator extends CSSRuleValidator { constructor() { super([ 'justify-content', 'align-content', 'place-content', // Shorthand <'align-content'> <'justify-content'>? 'align-items', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.FlexGrid; } getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined { if (!computedStyles) { return; } if (isFlexContainer(computedStyles) || isGridContainer(computedStyles)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display')); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedBySameElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedBySameElementRuleFix, { PROPERTY_NAME: buildPropertyName('display'), PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string), }), ); } } export class MulticolFlexGridValidator extends CSSRuleValidator { constructor() { super([ 'gap', 'column-gap', 'row-gap', 'grid-gap', 'grid-column-gap', 'grid-column-end', 'grid-row-gap', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.MulticolFlexGrid; } getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined { if (!computedStyles) { return; } if (isMulticolContainer(computedStyles) || isFlexContainer(computedStyles) || isGridContainer(computedStyles)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display')); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedBySameElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedBySameElementRuleFix, { PROPERTY_NAME: buildPropertyName('display'), PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string), }), ); } } export class PaddingValidator extends CSSRuleValidator { constructor() { super([ 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.Padding; } getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined { const display = computedStyles?.get('display'); if (!display) { return; } const tableAttributes = [ 'table-row-group', 'table-header-group', 'table-footer-group', 'table-row', 'table-column-group', 'table-column', ]; if (!tableAttributes.includes(display)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display')); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedBySameElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedBySameElementRuleFix, { PROPERTY_NAME: buildPropertyName('display'), PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string), }), ); } } export class PositionValidator extends CSSRuleValidator { constructor() { super([ 'top', 'right', 'bottom', 'left', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.Position; } getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined { const position = computedStyles?.get('position'); if (!position) { return; } if (position !== 'static') { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('position', computedStyles?.get('position')); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedBySameElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedBySameElementRuleFix, { PROPERTY_NAME: buildPropertyName('position'), PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('position') as string), }), ); } } export class ZIndexValidator extends CSSRuleValidator { constructor() { super([ 'z-index', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.ZIndex; } getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint |undefined { const position = computedStyles?.get('position'); if (!position) { return; } if (['absolute', 'relative', 'fixed', 'sticky'].includes(position) || isFlexContainer(parentComputedStyles) || isGridContainer(parentComputedStyles)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('position', computedStyles?.get('position')); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedBySameElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedBySameElementRuleFix, { PROPERTY_NAME: buildPropertyName('position'), PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('position') as string), }), ); } } /** * Validates if CSS width/height are having an effect on an element. * See "Applies to" in https://www.w3.org/TR/css-sizing-3/#propdef-width. * See "Applies to" in https://www.w3.org/TR/css-sizing-3/#propdef-height. */ export class SizingValidator extends CSSRuleValidator { constructor() { super([ 'width', 'height', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.Sizing; } getHint( propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>, nodeName?: string): Hint|undefined { if (!computedStyles || !nodeName) { return; } if (!isInlineElement(computedStyles)) { return; } // See https://html.spec.whatwg.org/multipage/rendering.html#replaced-elements. if (isPossiblyReplacedElement(nodeName)) { return; } const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display')); const affectedPropertyDeclarationCode = buildPropertyName(propertyName); return new Hint( i18nString(UIStrings.ruleViolatedBySameElementRuleReason, { 'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration, 'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode, }), i18nString(UIStrings.ruleViolatedBySameElementRuleFix, { PROPERTY_NAME: buildPropertyName('display'), PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string), }), ); } } /** * Checks that font variation settings are applicable to the actual font. */ export class FontVariationSettingsValidator extends CSSRuleValidator { constructor() { super([ 'font-variation-settings', ]); } override getMetricType(): Host.UserMetrics.CSSHintType { return Host.UserMetrics.CSSHintType.FontVariationSettings; } getHint( propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>, nodeName?: string, fontFaces?: Array<SDK.CSSFontFace.CSSFontFace>): Hint|undefined { if (!computedStyles) { return; } const value = computedStyles.get('font-variation-settings'); if (!value) { return; } const fontFamily = computedStyles.get('font-family'); if (!fontFamily) { return; } const fontFamilies = new Set<string>(SDK.CSSPropertyParser.parseFontFamily(fontFamily)); const matchingFontFaces = (fontFaces || []).filter(f => fontFamilies.has(f.getFontFamily())); const variationSettings = SDK.CSSPropertyParser.parseFontVariationSettings(value); const warnings = []; for (const elementSetting of variationSettings) { for (const font of matchingFontFaces) { const fontSetting = font.getVariationAxisByTag(elementSetting.tag); if (!fontSetting) { continue; } if (elementSetting.value < fontSetting.minValue || elementSetting.value > fontSetting.maxValue) { warnings.push(i18nString(UIStrings.fontVariationSettingsWarning, { PH1: elementSetting.tag, PH2: elementSetting.value, PH3: fontSetting.minValue, PH4: fontSetting.maxValue, PH5: font.getFontFamily(), })); } } } if (!warnings.length) { return; } return new Hint( warnings.join(' '), '', ); } } const CSS_RULE_VALIDATORS = [ AlignContentValidator, FlexContainerValidator, FlexGridValidator, FlexItemValidator, FlexOrGridItemValidator, FontVariationSettingsValidator, GridContainerValidator, GridItemValidator, MulticolFlexGridValidator, PaddingValidator, PositionValidator, SizingValidator, ZIndexValidator, ]; const setupCSSRulesValidators = (): Map<string, CSSRuleValidator[]> => { const validatorsMap = new Map<string, CSSRuleValidator[]>(); for (const validatorClass of CSS_RULE_VALIDATORS) { const validator = new validatorClass(); const affectedProperties = validator.getApplicableProperties(); for (const affectedProperty of affectedProperties) { let propertyValidators = validatorsMap.get(affectedProperty); if (propertyValidators === undefined) { propertyValidators = []; } propertyValidators.push(validator); validatorsMap.set(affectedProperty, propertyValidators); } } return validatorsMap; }; export const cssRuleValidatorsMap: Map<string, CSSRuleValidator[]> = setupCSSRulesValidators();