chrome-devtools-frontend
Version:
Chrome DevTools UI
509 lines (448 loc) • 18.7 kB
text/typescript
// 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 i18n from '../../core/i18n/i18n.js';
import {
buildStyledPropertyText,
buildStyledRuleText,
isFlexContainer,
isGridContainer,
isMulticolContainer,
} from './CSSRuleValidatorHelper.js';
const UIStrings = {
/**
*@description The type of the CSS rule validation message shown in the Style pane. "Deprecated property" means that a property in the declaration is deprecated and should not be used.
*/
deprecatedPropertyHintPrefix: 'Deprecated property',
/**
*@description The type of the CSS rule validation message shown in the Style pane. "Inactive property" means that a property declaration was valid syntactially but didn't have expected effect.
*/
inactivePropertyHintPrefix: 'Inactive property',
/**
*@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 on the same element bypasses the effect of {AFFECTED_PROPERTY_DECLARATION_CODE}.',
/**
*@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: nowrap} REASON_PROPERTY_DECLARATION_CODE
*/
ruleViolatedBySameElementRuleFix: 'Try removing {REASON_PROPERTY_DECLARATION_CODE} or changing its 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 changing 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 bypasses the effect of {AFFECTED_PROPERTY_DECLARATION_CODE}.',
/**
*@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 changing the {EXISTING_PARENT_ELEMENT_RULE} property on the parent to {TARGET_PARENT_ELEMENT_RULE}.',
};
const str_ = i18n.i18n.registerUIStrings('panels/elements/CSSRuleValidator.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export const enum AuthoringHintType {
INACTIVE_PROPERTY = 'ruleValidation',
DEPRECATED_PROPERTY = 'deprecatedProperty',
}
export class AuthoringHint {
readonly #hintType: AuthoringHintType;
readonly #hintMessage: string;
readonly #possibleFixMessage: string|null;
readonly #learnMoreLink: string|undefined;
constructor(
hintType: AuthoringHintType, hintMessage: string, possibleFixMessage: string|null, learnMoreLink?: string) {
this.#hintType = hintType;
this.#hintMessage = hintMessage;
this.#possibleFixMessage = possibleFixMessage;
this.#learnMoreLink = learnMoreLink;
}
getHintPrefix(): string {
switch (this.#hintType) {
case AuthoringHintType.INACTIVE_PROPERTY:
return i18nString(UIStrings.inactivePropertyHintPrefix);
case AuthoringHintType.DEPRECATED_PROPERTY:
return i18nString(UIStrings.deprecatedPropertyHintPrefix);
}
}
getHintMessage(): string {
return this.#hintMessage;
}
getPossibleFixMessage(): string|null {
return this.#possibleFixMessage;
}
getLearnMoreLink(): string|undefined {
return this.#learnMoreLink;
}
}
export abstract class CSSRuleValidator {
readonly #affectedProperties: string[];
constructor(affectedProperties: string[]) {
this.#affectedProperties = affectedProperties;
}
/**
* If `isRuleValid` returns false, it means there is a hint to be shown. The hint is retrieved by invoking `getAuthoringHint`.
*/
abstract isRuleValid(computedStyles: Map<String, String>|null, parentsComputedStyles?: Map<String, String>|null):
boolean;
getAffectedProperties(): string[] {
return this.#affectedProperties;
}
abstract getAuthoringHint(
propertyName: string, computedStyles: Map<String, String>|null,
parentComputedStyles: Map<String, String>|null): AuthoringHint;
}
export class AlignContentValidator extends CSSRuleValidator {
constructor() {
super(['align-content']);
}
isRuleValid(computedStyles: Map<String, String>|null): boolean {
if (computedStyles === null || computedStyles === undefined) {
return true;
}
if (!isFlexContainer(computedStyles)) {
return true;
}
return computedStyles.get('flex-wrap') !== 'nowrap';
}
getAuthoringHint(): AuthoringHint {
const reasonPropertyDeclaration = buildStyledPropertyText('flex-wrap');
const affectedPropertyDeclarationCode = buildStyledPropertyText('align-content');
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
}),
);
}
}
export class FlexItemValidator extends CSSRuleValidator {
constructor() {
super(['flex', 'flex-basis', 'flex-grow', 'flex-shrink']);
}
isRuleValid(computedStyles: Map<String, String>|null, parentsComputedStyles: Map<String, String>|null): boolean {
if (parentsComputedStyles === null) {
return true;
}
return isFlexContainer(parentsComputedStyles);
}
getAuthoringHint(
property: string, computedStyles: Map<String, String>|null,
parentsComputedStyles: Map<String, String>|null): AuthoringHint {
const reasonPropertyDeclaration = buildStyledRuleText('display', parentsComputedStyles?.get('display'));
const affectedPropertyDeclarationCode = buildStyledPropertyText(property);
const targeParentPropertyDeclaration = buildStyledRuleText('display', 'flex');
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
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': targeParentPropertyDeclaration,
}),
);
}
}
export class FlexContainerValidator extends CSSRuleValidator {
constructor() {
super(['flex-direction', 'flex-flow', 'flex-wrap', 'justify-content']);
}
isRuleValid(computedStyles: Map<String, String>|null): boolean {
if (computedStyles === null) {
return true;
}
return isFlexContainer(computedStyles);
}
getAuthoringHint(property: string, computedStyles: Map<String, String>|null): AuthoringHint {
const reasonPropertyDeclaration = buildStyledRuleText('display', computedStyles?.get('display'));
const targetRuleCode = buildStyledRuleText('display', 'flex');
const affectedPropertyDeclarationCode = buildStyledPropertyText(property);
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
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',
]);
}
isRuleValid(computedStyles: Map<String, String>|null): boolean {
if (computedStyles === null) {
return true;
}
return isGridContainer(computedStyles);
}
getAuthoringHint(property: string, computedStyles: Map<String, String>|null): AuthoringHint {
const reasonPropertyDeclaration = buildStyledRuleText('display', computedStyles?.get('display'));
const targetRuleCode = buildStyledRuleText('display', 'grid');
const affectedPropertyDeclarationCode = buildStyledPropertyText(property);
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
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',
]);
}
isRuleValid(computedStyles: Map<String, String>|null, parentComputedStyles: Map<String, String>|null): boolean {
if (parentComputedStyles === null) {
return true;
}
return isGridContainer(parentComputedStyles);
}
getAuthoringHint(
property: string, computedStyles: Map<String, String>|null,
parentComputedStyles: Map<String, String>|null): AuthoringHint {
const reasonPropertyDeclaration = buildStyledRuleText('display', parentComputedStyles?.get('display'));
const targeParentPropertyDeclaration = buildStyledRuleText('display', 'grid');
const affectedPropertyDeclarationCode = buildStyledPropertyText(property);
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
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': targeParentPropertyDeclaration,
}),
);
}
}
export class FlexGridValidator extends CSSRuleValidator {
constructor() {
super([
'order',
'align-content',
'align-items',
'align-self',
]);
}
isRuleValid(computedStyles: Map<String, String>|null): boolean {
if (computedStyles === null) {
return true;
}
return isFlexContainer(computedStyles) || isGridContainer(computedStyles);
}
getAuthoringHint(property: string, computedStyles: Map<String, String>|null): AuthoringHint {
const reasonPropertyDeclaration = buildStyledRuleText('display', computedStyles?.get('display'));
const affectedPropertyDeclarationCode = buildStyledPropertyText(property);
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
}),
);
}
}
export class MulticolFlexGridValidator extends CSSRuleValidator {
constructor() {
super([
'gap',
'column-gap',
'row-gap',
'grid-gap',
'grid-column-gap',
'grid-column-end',
'grid-row-gap',
]);
}
isRuleValid(computedStyles: Map<String, String>|null): boolean {
if (computedStyles === null) {
return true;
}
return isMulticolContainer(computedStyles) || isFlexContainer(computedStyles) || isGridContainer(computedStyles);
}
getAuthoringHint(property: string, computedStyles: Map<String, String>|null): AuthoringHint {
const reasonPropertyDeclaration = buildStyledRuleText('display', computedStyles?.get('display'));
const affectedPropertyDeclarationCode = buildStyledPropertyText(property);
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
}),
);
}
}
export class PaddingValidator extends CSSRuleValidator {
constructor() {
super([
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
]);
}
isRuleValid(computedStyles: Map<String, String>|null): boolean {
const display = computedStyles?.get('display');
if (display === null || display === undefined) {
return true;
}
return !['table-row-group', 'table-header-group', 'table-footer-group', 'table-row', 'table-column-group',
'table-column']
.includes(display as string);
}
getAuthoringHint(property: string, computedStyles: Map<String, String>|null): AuthoringHint {
const reasonPropertyDeclaration = buildStyledRuleText('display', computedStyles?.get('display'));
const affectedPropertyDeclarationCode = buildStyledPropertyText(property);
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
}),
);
}
}
export class PositionValidator extends CSSRuleValidator {
constructor() {
super([
'top',
'right',
'bottom',
'left',
]);
}
isRuleValid(computedStyles: Map<String, String>|null): boolean {
const position = computedStyles?.get('position');
if (position === null || position === undefined) {
return true;
}
return position !== 'static';
}
getAuthoringHint(property: string, computedStyles: Map<String, String>|null): AuthoringHint {
const reasonPropertyDeclaration = buildStyledRuleText('position', computedStyles?.get('position'));
const affectedPropertyDeclarationCode = buildStyledPropertyText(property);
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
}),
);
}
}
export class ZIndexValidator extends CSSRuleValidator {
constructor() {
super([
'z-index',
]);
}
isRuleValid(computedStyles: Map<String, String>|null, parentComputedStyles: Map<String, String>|null): boolean {
const position = computedStyles?.get('position');
if (position === null || position === undefined) {
return true;
}
return ['absolute', 'relative', 'fixed', 'sticky'].includes(position as string) ||
isFlexContainer(parentComputedStyles);
}
getAuthoringHint(property: string, computedStyles: Map<String, String>|null): AuthoringHint {
const reasonPropertyDeclaration = buildStyledRuleText('position', computedStyles?.get('position'));
const affectedPropertyDeclarationCode = buildStyledPropertyText(property);
return new AuthoringHint(
AuthoringHintType.INACTIVE_PROPERTY,
i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
'AFFECTED_PROPERTY_DECLARATION_CODE': affectedPropertyDeclarationCode,
}),
i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
'REASON_PROPERTY_DECLARATION_CODE': reasonPropertyDeclaration,
}),
);
}
}
const CSS_RULE_VALIDATORS = [
AlignContentValidator,
FlexItemValidator,
FlexContainerValidator,
GridContainerValidator,
GridItemValidator,
FlexGridValidator,
MulticolFlexGridValidator,
PaddingValidator,
PositionValidator,
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.getAffectedProperties();
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();