UNPKG

chrome-devtools-frontend

Version:
478 lines (418 loc) • 14.7 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 * as Protocol from '../../generated/protocol.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as Platform from '../platform/platform.js'; import {CSSContainerQuery} from './CSSContainerQuery.js'; import {CSSLayer} from './CSSLayer.js'; import {CSSMedia} from './CSSMedia.js'; import type {CSSModel, Edit} from './CSSModel.js'; import {CSSScope} from './CSSScope.js'; import {CSSStartingStyle} from './CSSStartingStyle.js'; import {CSSStyleDeclaration, Type} from './CSSStyleDeclaration.js'; import type {CSSStyleSheetHeader} from './CSSStyleSheetHeader.js'; import {CSSSupports} from './CSSSupports.js'; function styleSheetHeaderForRule( cssModel: CSSModel, {styleSheetId}: {styleSheetId?: Protocol.DOM.StyleSheetId}): CSSStyleSheetHeader|null { return styleSheetId && cssModel.styleSheetHeaderForId(styleSheetId) || null; } export class CSSRule { readonly cssModelInternal: CSSModel; readonly origin: Protocol.CSS.StyleSheetOrigin; readonly style: CSSStyleDeclaration; readonly header: CSSStyleSheetHeader|null; readonly treeScope: Protocol.DOM.BackendNodeId|undefined; constructor(cssModel: CSSModel, payload: { style: Protocol.CSS.CSSStyle, origin: Protocol.CSS.StyleSheetOrigin, originTreeScopeNodeId: Protocol.DOM.BackendNodeId|undefined, header: CSSStyleSheetHeader|null, }) { this.header = payload.header; this.cssModelInternal = cssModel; this.origin = payload.origin; this.treeScope = payload.originTreeScopeNodeId; this.style = new CSSStyleDeclaration(this.cssModelInternal, this, payload.style, Type.Regular); } get sourceURL(): string|undefined { return this.header?.sourceURL; } rebase(edit: Edit): void { if (this.header?.id !== edit.styleSheetId) { return; } this.style.rebase(edit); } resourceURL(): Platform.DevToolsPath.UrlString { return this.header?.resourceURL() ?? Platform.DevToolsPath.EmptyUrlString; } isUserAgent(): boolean { return this.origin === Protocol.CSS.StyleSheetOrigin.UserAgent; } isInjected(): boolean { return this.origin === Protocol.CSS.StyleSheetOrigin.Injected; } isViaInspector(): boolean { return this.origin === Protocol.CSS.StyleSheetOrigin.Inspector; } isRegular(): boolean { return this.origin === Protocol.CSS.StyleSheetOrigin.Regular; } isKeyframeRule(): boolean { return false; } cssModel(): CSSModel { return this.cssModelInternal; } } class CSSValue { text: string; range?: TextUtils.TextRange.TextRange; specificity?: Protocol.CSS.Specificity; constructor(payload: Protocol.CSS.Value) { this.text = payload.text; if (payload.range) { this.range = TextUtils.TextRange.TextRange.fromObject(payload.range); } if (payload.specificity) { this.specificity = payload.specificity; } } rebase(edit: Edit): void { if (!this.range) { return; } this.range = this.range.rebaseAfterTextEdit(edit.oldRange, edit.newRange); } } export class CSSStyleRule extends CSSRule { selectors!: CSSValue[]; nestingSelectors?: string[]; media: CSSMedia[]; containerQueries: CSSContainerQuery[]; supports: CSSSupports[]; scopes: CSSScope[]; layers: CSSLayer[]; ruleTypes: Protocol.CSS.CSSRuleType[]; startingStyles: CSSStartingStyle[]; wasUsed: boolean; constructor(cssModel: CSSModel, payload: Protocol.CSS.CSSRule, wasUsed?: boolean) { super(cssModel, { origin: payload.origin, style: payload.style, header: styleSheetHeaderForRule(cssModel, payload), originTreeScopeNodeId: payload.originTreeScopeNodeId }); this.reinitializeSelectors(payload.selectorList); this.nestingSelectors = payload.nestingSelectors; this.media = payload.media ? CSSMedia.parseMediaArrayPayload(cssModel, payload.media) : []; this.containerQueries = payload.containerQueries ? CSSContainerQuery.parseContainerQueriesPayload(cssModel, payload.containerQueries) : []; this.scopes = payload.scopes ? CSSScope.parseScopesPayload(cssModel, payload.scopes) : []; this.supports = payload.supports ? CSSSupports.parseSupportsPayload(cssModel, payload.supports) : []; this.layers = payload.layers ? CSSLayer.parseLayerPayload(cssModel, payload.layers) : []; this.startingStyles = payload.startingStyles ? CSSStartingStyle.parseStartingStylePayload(cssModel, payload.startingStyles) : []; this.ruleTypes = payload.ruleTypes || []; this.wasUsed = wasUsed || false; } static createDummyRule(cssModel: CSSModel, selectorText: string): CSSStyleRule { const dummyPayload = { selectorList: { text: '', selectors: [{text: selectorText, value: undefined}], }, style: { styleSheetId: '0' as Protocol.DOM.StyleSheetId, range: new TextUtils.TextRange.TextRange(0, 0, 0, 0), shorthandEntries: [], cssProperties: [], }, origin: Protocol.CSS.StyleSheetOrigin.Inspector, }; return new CSSStyleRule(cssModel, (dummyPayload as Protocol.CSS.CSSRule)); } private reinitializeSelectors(selectorList: Protocol.CSS.SelectorList): void { this.selectors = []; for (let i = 0; i < selectorList.selectors.length; ++i) { this.selectors.push(new CSSValue(selectorList.selectors[i])); } } setSelectorText(newSelector: string): Promise<boolean> { const styleSheetId = this.header?.id; if (!styleSheetId) { throw new Error('No rule stylesheet id'); } const range = this.selectorRange(); if (!range) { throw new Error('Rule selector is not editable'); } return this.cssModelInternal.setSelectorText(styleSheetId, range, newSelector); } selectorText(): string { return this.selectors.map(selector => selector.text).join(', '); } selectorRange(): TextUtils.TextRange.TextRange|null { // Nested group rules might not contain a selector. // https://www.w3.org/TR/css-nesting-1/#conditionals if (this.selectors.length === 0) { return null; } const firstRange = this.selectors[0].range; const lastRange = this.selectors[this.selectors.length - 1].range; if (!firstRange || !lastRange) { return null; } return new TextUtils.TextRange.TextRange( firstRange.startLine, firstRange.startColumn, lastRange.endLine, lastRange.endColumn); } lineNumberInSource(selectorIndex: number): number { const selector = this.selectors[selectorIndex]; if (!selector?.range || !this.header) { return 0; } return this.header.lineNumberInSource(selector.range.startLine); } columnNumberInSource(selectorIndex: number): number|undefined { const selector = this.selectors[selectorIndex]; if (!selector?.range || !this.header) { return undefined; } return this.header.columnNumberInSource(selector.range.startLine, selector.range.startColumn); } override rebase(edit: Edit): void { if (this.header?.id !== edit.styleSheetId) { return; } const range = this.selectorRange(); if (range?.equal(edit.oldRange)) { this.reinitializeSelectors((edit.payload as Protocol.CSS.SelectorList)); } else { for (let i = 0; i < this.selectors.length; ++i) { this.selectors[i].rebase(edit); } } this.media.forEach(media => media.rebase(edit)); this.containerQueries.forEach(cq => cq.rebase(edit)); this.scopes.forEach(scope => scope.rebase(edit)); this.supports.forEach(supports => supports.rebase(edit)); super.rebase(edit); } } export class CSSPropertyRule extends CSSRule { #name: CSSValue; constructor(cssModel: CSSModel, payload: Protocol.CSS.CSSPropertyRule) { super(cssModel, { origin: payload.origin, style: payload.style, header: styleSheetHeaderForRule(cssModel, payload), originTreeScopeNodeId: undefined, }); this.#name = new CSSValue(payload.propertyName); } propertyName(): CSSValue { return this.#name; } initialValue(): string|null { return this.style.hasActiveProperty('initial-value') ? this.style.getPropertyValue('initial-value') : null; } syntax(): string { return this.style.getPropertyValue('syntax'); } inherits(): boolean { return this.style.getPropertyValue('inherits') === 'true'; } setPropertyName(newPropertyName: string): Promise<boolean> { const styleSheetId = this.header?.id; if (!styleSheetId) { throw new Error('No rule stylesheet id'); } const range = this.#name.range; if (!range) { throw new Error('Property name is not editable'); } return this.cssModelInternal.setPropertyRulePropertyName(styleSheetId, range, newPropertyName); } } export class CSSAtRule extends CSSRule { readonly #name: CSSValue|null; readonly #type: string; readonly #subsection: string|null; constructor(cssModel: CSSModel, payload: Protocol.CSS.CSSAtRule) { super(cssModel, { origin: payload.origin, style: payload.style, header: styleSheetHeaderForRule(cssModel, payload), originTreeScopeNodeId: undefined }); this.#name = payload.name ? new CSSValue(payload.name) : null; this.#type = payload.type; this.#subsection = payload.subsection ?? null; } name(): CSSValue|null { return this.#name; } type(): string { return this.#type; } subsection(): string|null { return this.#subsection; } } export class CSSKeyframesRule { readonly #animationName: CSSValue; readonly #keyframes: CSSKeyframeRule[]; constructor(cssModel: CSSModel, payload: Protocol.CSS.CSSKeyframesRule) { this.#animationName = new CSSValue(payload.animationName); this.#keyframes = payload.keyframes.map(keyframeRule => new CSSKeyframeRule(cssModel, keyframeRule, this.#animationName.text)); } name(): CSSValue { return this.#animationName; } keyframes(): CSSKeyframeRule[] { return this.#keyframes; } } export class CSSKeyframeRule extends CSSRule { #keyText!: CSSValue; #parentRuleName: string; constructor(cssModel: CSSModel, payload: Protocol.CSS.CSSKeyframeRule, parentRuleName: string) { super(cssModel, { origin: payload.origin, style: payload.style, header: styleSheetHeaderForRule(cssModel, payload), originTreeScopeNodeId: undefined }); this.reinitializeKey(payload.keyText); this.#parentRuleName = parentRuleName; } parentRuleName(): string { return this.#parentRuleName; } key(): CSSValue { return this.#keyText; } private reinitializeKey(payload: Protocol.CSS.Value): void { this.#keyText = new CSSValue(payload); } override rebase(edit: Edit): void { if (this.header?.id !== edit.styleSheetId || !this.#keyText.range) { return; } if (edit.oldRange.equal(this.#keyText.range)) { this.reinitializeKey((edit.payload as Protocol.CSS.Value)); } else { this.#keyText.rebase(edit); } super.rebase(edit); } override isKeyframeRule(): boolean { return true; } setKeyText(newKeyText: string): Promise<boolean> { const styleSheetId = this.header?.id; if (!styleSheetId) { throw new Error('No rule stylesheet id'); } const range = this.#keyText.range; if (!range) { throw new Error('Keyframe key is not editable'); } return this.cssModelInternal.setKeyframeKey(styleSheetId, range, newKeyText); } } export class CSSPositionTryRule extends CSSRule { readonly #name: CSSValue; readonly #active: boolean; constructor(cssModel: CSSModel, payload: Protocol.CSS.CSSPositionTryRule) { super(cssModel, { origin: payload.origin, style: payload.style, header: styleSheetHeaderForRule(cssModel, payload), originTreeScopeNodeId: undefined }); this.#name = new CSSValue(payload.name); this.#active = payload.active; } name(): CSSValue { return this.#name; } active(): boolean { return this.#active; } } export interface CSSNestedStyleLeaf { style: CSSStyleDeclaration; } export type CSSNestedStyleCondition = { children: CSSNestedStyle[], }&({media: CSSMedia}|{container: CSSContainerQuery}|{supports: CSSSupports}); export type CSSNestedStyle = CSSNestedStyleLeaf|CSSNestedStyleCondition; export class CSSFunctionRule extends CSSRule { readonly #name: CSSValue; readonly #parameters: string[]; readonly #children: CSSNestedStyle[]; constructor(cssModel: CSSModel, payload: Protocol.CSS.CSSFunctionRule) { super(cssModel, { origin: payload.origin, style: {cssProperties: [], shorthandEntries: []}, header: styleSheetHeaderForRule(cssModel, payload), originTreeScopeNodeId: undefined }); this.#name = new CSSValue(payload.name); this.#parameters = payload.parameters.map(({name}) => name); this.#children = this.protocolNodesToNestedStyles(payload.children); } functionName(): CSSValue { return this.#name; } parameters(): string[] { return this.#parameters; } children(): CSSNestedStyle[] { return this.#children; } nameWithParameters(): string { return `${this.functionName().text}(${this.parameters().join(', ')})`; } protocolNodesToNestedStyles(nodes: Protocol.CSS.CSSFunctionNode[]): CSSNestedStyle[] { const result = []; for (const node of nodes) { const nestedStyle = this.protocolNodeToNestedStyle(node); if (nestedStyle) { result.push(nestedStyle); } } return result; } protocolNodeToNestedStyle(node: Protocol.CSS.CSSFunctionNode): CSSNestedStyle|undefined { if (node.style) { return {style: new CSSStyleDeclaration(this.cssModelInternal, this, node.style, Type.Regular)}; } if (node.condition) { const children = this.protocolNodesToNestedStyles(node.condition.children); if (node.condition.media) { return {children, media: new CSSMedia(this.cssModelInternal, node.condition.media)}; } if (node.condition.containerQueries) { return { children, container: new CSSContainerQuery(this.cssModelInternal, node.condition.containerQueries), }; } if (node.condition.supports) { return { children, supports: new CSSSupports(this.cssModelInternal, node.condition.supports), }; } console.error('A function rule condition must have a media, container, or supports'); return; } console.error('A function rule node must have a style or condition'); return; } }