UNPKG

chrome-devtools-frontend

Version:
314 lines (275 loc) • 10.8 kB
// Copyright 2016 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import type * as Protocol from '../../generated/protocol.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import {cssMetadata} from './CSSMetadata.js'; import type {CSSModel, Edit} from './CSSModel.js'; import {CSSProperty} from './CSSProperty.js'; import type {CSSRule} from './CSSRule.js'; import type {Target} from './Target.js'; export class CSSStyleDeclaration { readonly #cssModel: CSSModel; parentRule: CSSRule|null; #allProperties: CSSProperty[] = []; styleSheetId?: Protocol.DOM.StyleSheetId; range: TextUtils.TextRange.TextRange|null = null; cssText?: string; #shorthandValues = new Map<string, string>(); #shorthandIsImportant = new Set<string>(); #activePropertyMap = new Map<string, CSSProperty>(); #leadingProperties: CSSProperty[]|null = null; type: Type; // For CSSStyles coming from animations, // This holds the name of the animation. #animationName?: string; constructor( cssModel: CSSModel, parentRule: CSSRule|null, payload: Protocol.CSS.CSSStyle, type: Type, animationName?: string) { this.#cssModel = cssModel; this.parentRule = parentRule; this.#reinitialize(payload); this.type = type; this.#animationName = animationName; } rebase(edit: Edit): void { if (this.styleSheetId !== edit.styleSheetId || !this.range) { return; } if (edit.oldRange.equal(this.range)) { this.#reinitialize((edit.payload as Protocol.CSS.CSSStyle)); } else { this.range = this.range.rebaseAfterTextEdit(edit.oldRange, edit.newRange); for (let i = 0; i < this.#allProperties.length; ++i) { this.#allProperties[i].rebase(edit); } } } animationName(): string|undefined { return this.#animationName; } #reinitialize(payload: Protocol.CSS.CSSStyle): void { this.styleSheetId = payload.styleSheetId; this.range = payload.range ? TextUtils.TextRange.TextRange.fromObject(payload.range) : null; const shorthandEntries = payload.shorthandEntries; this.#shorthandValues = new Map(); this.#shorthandIsImportant = new Set(); for (let i = 0; i < shorthandEntries.length; ++i) { this.#shorthandValues.set(shorthandEntries[i].name, shorthandEntries[i].value); if (shorthandEntries[i].important) { this.#shorthandIsImportant.add(shorthandEntries[i].name); } } this.#allProperties = []; if (payload.cssText && this.range) { const longhands = []; for (const cssProperty of payload.cssProperties) { const range = cssProperty.range; if (!range) { continue; } const parsedProperty = CSSProperty.parsePayload(this, this.#allProperties.length, cssProperty); this.#allProperties.push(parsedProperty); for (const longhand of parsedProperty.getLonghandProperties()) { longhands.push(longhand); } } for (const longhand of longhands) { longhand.index = this.#allProperties.length; this.#allProperties.push(longhand); } } else { for (const cssProperty of payload.cssProperties) { this.#allProperties.push(CSSProperty.parsePayload(this, this.#allProperties.length, cssProperty)); } } this.#generateSyntheticPropertiesIfNeeded(); this.#computeInactiveProperties(); // TODO(changhaohan): verify if this #activePropertyMap is still necessary, or if it is // providing different information against the activeness in #allProperties. this.#activePropertyMap = new Map(); for (const property of this.#allProperties) { if (!property.activeInStyle()) { continue; } this.#activePropertyMap.set(property.name, property); } this.cssText = payload.cssText; this.#leadingProperties = null; } #generateSyntheticPropertiesIfNeeded(): void { if (this.range) { return; } if (!this.#shorthandValues.size) { return; } const propertiesSet = new Set<string>(); for (const property of this.#allProperties) { propertiesSet.add(property.name); } const generatedProperties = []; // For style-based properties, generate #shorthands with values when possible. for (const property of this.#allProperties) { // For style-based properties, try generating #shorthands. const shorthands = cssMetadata().getShorthands(property.name) || []; for (const shorthand of shorthands) { if (propertiesSet.has(shorthand)) { continue; } // There already is a shorthand this #longhand falls under. const shorthandValue = this.#shorthandValues.get(shorthand); if (!shorthandValue) { continue; } // Never generate synthetic #shorthands when no value is available. // Generate synthetic shorthand we have a value for. const shorthandImportance = Boolean(this.#shorthandIsImportant.has(shorthand)); const shorthandProperty = new CSSProperty( this, this.allProperties().length, shorthand, shorthandValue, shorthandImportance, false, true, false); generatedProperties.push(shorthandProperty); propertiesSet.add(shorthand); } } this.#allProperties = this.#allProperties.concat(generatedProperties); } #computeLeadingProperties(): CSSProperty[] { function propertyHasRange(property: CSSProperty): boolean { return Boolean(property.range); } if (this.range) { return this.#allProperties.filter(propertyHasRange); } const leadingProperties = []; for (const property of this.#allProperties) { const shorthands = cssMetadata().getShorthands(property.name) || []; let belongToAnyShorthand = false; for (const shorthand of shorthands) { if (this.#shorthandValues.get(shorthand)) { belongToAnyShorthand = true; break; } } if (!belongToAnyShorthand) { leadingProperties.push(property); } } return leadingProperties; } leadingProperties(): CSSProperty[] { if (!this.#leadingProperties) { this.#leadingProperties = this.#computeLeadingProperties(); } return this.#leadingProperties; } target(): Target { return this.#cssModel.target(); } cssModel(): CSSModel { return this.#cssModel; } #computeInactiveProperties(): void { const activeProperties = new Map<string, CSSProperty>(); // The order of the properties are: // 1. regular property, including shorthands // 2. longhand components from shorthands, in the order of their shorthands. const processedLonghands = new Set(); for (const property of this.#allProperties) { const metadata = cssMetadata(); const canonicalName = metadata.canonicalPropertyName(property.name); if (property.disabled || !property.parsedOk) { if (!property.disabled && metadata.isCustomProperty(property.name)) { // Variable declarations that aren't parsedOk still "overload" other previous active declarations. activeProperties.get(canonicalName)?.setActive(false); activeProperties.delete(canonicalName); } property.setActive(false); continue; } if (processedLonghands.has(property)) { continue; } for (const longhand of property.getLonghandProperties()) { const activeLonghand = activeProperties.get(longhand.name); if (!activeLonghand) { activeProperties.set(longhand.name, longhand); } else if (!activeLonghand.important || longhand.important) { activeLonghand.setActive(false); activeProperties.set(longhand.name, longhand); } else { longhand.setActive(false); } processedLonghands.add(longhand); } const activeProperty = activeProperties.get(canonicalName); if (!activeProperty) { activeProperties.set(canonicalName, property); } else if (!activeProperty.important || property.important) { activeProperty.setActive(false); activeProperties.set(canonicalName, property); } else { property.setActive(false); } } } allProperties(): CSSProperty[] { return this.#allProperties; } hasActiveProperty(name: string): boolean { return this.#activePropertyMap.has(name); } getPropertyValue(name: string): string { const property = this.#activePropertyMap.get(name); return property ? property.value : ''; } isPropertyImplicit(name: string): boolean { const property = this.#activePropertyMap.get(name); return property ? property.implicit : false; } propertyAt(index: number): CSSProperty|null { return (index < this.allProperties().length) ? this.allProperties()[index] : null; } pastLastSourcePropertyIndex(): number { for (let i = this.allProperties().length - 1; i >= 0; --i) { if (this.allProperties()[i].range) { return i + 1; } } return 0; } #insertionRange(index: number): TextUtils.TextRange.TextRange { const property = this.propertyAt(index); if (property?.range) { return property.range.collapseToStart(); } if (!this.range) { throw new Error('CSSStyleDeclaration.range is null'); } return this.range.collapseToEnd(); } newBlankProperty(index?: number): CSSProperty { index = (typeof index === 'undefined') ? this.pastLastSourcePropertyIndex() : index; const property = new CSSProperty(this, index, '', '', false, false, true, false, '', this.#insertionRange(index)); return property; } setText(text: string, majorChange: boolean): Promise<boolean> { if (!this.range || !this.styleSheetId) { return Promise.resolve(false); } return this.#cssModel.setStyleText(this.styleSheetId, this.range, text, majorChange); } insertPropertyAt(index: number, name: string, value: string, userCallback?: ((arg0: boolean) => void)): void { void this.newBlankProperty(index).setText(name + ': ' + value + ';', false, true).then(userCallback); } appendProperty(name: string, value: string, userCallback?: ((arg0: boolean) => void)): void { this.insertPropertyAt(this.allProperties().length, name, value, userCallback); } } export enum Type { /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */ Regular = 'Regular', Inline = 'Inline', Attributes = 'Attributes', Pseudo = 'Pseudo', // This type is for style declarations generated by devtools Transition = 'Transition', Animation = 'Animation', /* eslint-enable @typescript-eslint/naming-convention */ }