import * as PropertySymbol from '../../../PropertySymbol.js'; import CSSStyleDeclarationPropertyManager from '../property-manager/CSSStyleDeclarationPropertyManager.js'; import NodeTypeEnum from '../../../nodes/node/NodeTypeEnum.js'; import CSSRuleTypeEnum from '../../CSSRuleTypeEnum.js'; import CSSStyleDeclarationElementDefaultCSS from './config/CSSStyleDeclarationElementDefaultCSS.js'; import CSSStyleDeclarationElementInheritedProperties from './config/CSSStyleDeclarationElementInheritedProperties.js'; import CSSStyleDeclarationElementMeasurementProperties from './config/CSSStyleDeclarationElementMeasurementProperties.js'; import CSSStyleDeclarationCSSParser from '../css-parser/CSSStyleDeclarationCSSParser.js'; import QuerySelector from '../../../query-selector/QuerySelector.js'; import CSSMeasurementConverter from '../measurement-converter/CSSMeasurementConverter.js'; import MediaQueryList from '../../../match-media/MediaQueryList.js'; import WindowBrowserContext from '../../../window/WindowBrowserContext.js'; import CSSScopeRule from '../../rules/CSSScopeRule.js'; const CSS_MEASUREMENT_REGEXP = /[0-9.]+(px|rem|em|vw|vh|%|vmin|vmax|cm|mm|in|pt|pc|Q)/g; const HOST_REGEXP = /:host\s*\(([^)]+)\)|:host-context\s*\(([^)]+)\)/; const SINGLE_CSS_VARIABLE_REGEXP = /var\( *(--[^), ]+)\)/; const CSS_VARIABLE_REGEXP = /var\( *(--[^), ]+), *([^), ]+)\)/; /** * CSS Style Declaration utility */ export default class CSSStyleDeclarationComputedStyle { element; /** * Constructor. * * @param element Element. * @param [computed] Computed. */ constructor(element) { this.element = element; } /** * Returns style sheets. * * @param element Element. * @returns Style sheets. */ getComputedStyle() { const documentElements = []; const parentElements = []; let styleAndElement = { element: this.element, cssTexts: [] }; let shadowRootElements = []; let customElements = []; if (!this.element[PropertySymbol.isConnected]) { return new CSSStyleDeclarationPropertyManager(); } const cacheResult = this.element[PropertySymbol.cache].computedStyle; if (cacheResult?.result) { const result = cacheResult.result.deref(); if (result) { return result; } } // Walks through all parent elements and stores them in an array with element and matching CSS text. while (styleAndElement.element) { if (styleAndElement.element[PropertySymbol.nodeType] === NodeTypeEnum.elementNode) { const rootNode = styleAndElement.element.getRootNode(); if (rootNode[PropertySymbol.nodeType] === NodeTypeEnum.documentNode) { documentElements.unshift(styleAndElement); } else { shadowRootElements.unshift(styleAndElement); } if (styleAndElement.element.shadowRoot) { customElements.push(styleAndElement); } parentElements.unshift(styleAndElement); } if (styleAndElement.element === this.element[PropertySymbol.ownerDocument]) { const styleSheets = this.getStyleSheets(this.element[PropertySymbol.ownerDocument]); for (const styleSheet of styleSheets) { this.parseCSSRules({ elements: documentElements, cssRules: styleSheet.cssRules }); } // We need to parse ":host" and ":host-context" rules for custom elements. for (const customElement of customElements) { const styleSheets = this.getStyleSheets(customElement.element.shadowRoot); for (const styleSheet of styleSheets) { this.parseCSSRules({ elements: [], cssRules: styleSheet.cssRules, hostElement: customElement }); } } styleAndElement = { element: null, cssTexts: [] }; } else if (styleAndElement.element[PropertySymbol.nodeType] === NodeTypeEnum.documentFragmentNode && styleAndElement.element.host) { const shadowRoot = styleAndElement.element; const styleSheets = this.getStyleSheets(shadowRoot); styleAndElement = { element: shadowRoot.host, cssTexts: [] }; for (const styleSheet of styleSheets) { this.parseCSSRules({ elements: shadowRootElements, cssRules: styleSheet.cssRules, hostElement: styleAndElement }); } // We need to parse ":host" and ":host-context" rules for custom elements. for (const customElement of customElements) { const styleSheets = this.getStyleSheets(customElement.element.shadowRoot); for (const styleSheet of styleSheets) { this.parseCSSRules({ elements: [], cssRules: styleSheet.cssRules, hostElement: customElement }); } } customElements = []; shadowRootElements = []; } else { styleAndElement = { element: styleAndElement.element[PropertySymbol.parentNode], cssTexts: [] }; } } // Concatenates all parent element CSS to one string. const targetElement = parentElements[parentElements.length - 1]; const propertyManager = new CSSStyleDeclarationPropertyManager(); const cssProperties = {}; let rootFontSize = 16; let parentFontSize = 16; for (const parentElement of parentElements) { parentElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight); const defaultCSS = CSSStyleDeclarationElementDefaultCSS[parentElement.element[PropertySymbol.tagName]]; let elementCSSText = ''; if (defaultCSS) { if (typeof defaultCSS === 'string') { elementCSSText += defaultCSS; } else { for (const key of Object.keys(defaultCSS)) { if (key === 'default' || !!parentElement.element[key]) { elementCSSText += defaultCSS[key]; } } } } for (const cssText of parentElement.cssTexts) { elementCSSText += cssText.cssText; } const elementStyleAttribute = parentElement.element.getAttribute('style'); if (elementStyleAttribute) { elementCSSText += elementStyleAttribute; } const rulesAndProperties = CSSStyleDeclarationCSSParser.parse(elementCSSText); const rules = rulesAndProperties.rules; Object.assign(cssProperties, rulesAndProperties.properties); for (const { name, value, important } of rules) { if (CSSStyleDeclarationElementInheritedProperties[name] || parentElement === targetElement) { const parsedValue = this.parseCSSVariablesInValue(value.trim(), cssProperties); if (parsedValue && (!propertyManager.get(name)?.important || important)) { propertyManager.set(name, parsedValue, important); if (name === 'font' || name === 'font-size') { const fontSize = propertyManager.properties['font-size']; if (fontSize !== null) { const parsedValue = this.parseMeasurementsInValue({ value: fontSize.value, rootFontSize, parentFontSize, parentSize: parentFontSize }); if (parentElement.element[PropertySymbol.tagName] === 'HTML') { rootFontSize = parsedValue; } else if (parentElement !== targetElement) { parentFontSize = parsedValue; } } } } } } } for (const name of CSSStyleDeclarationElementMeasurementProperties) { const property = propertyManager.properties[name]; if (property) { property.value = this.parseMeasurementsInValue({ value: property.value, rootFontSize, parentFontSize, // TODO: Only "font-size" is supported when using percentage values. Add support for other properties. parentSize: name === 'font-size' ? parentFontSize : null }); } } const cachedResult = { result: new WeakRef(propertyManager) }; this.element[PropertySymbol.cache].computedStyle = cachedResult; this.element[PropertySymbol.ownerDocument][PropertySymbol.affectsComputedStyleCache].push(cachedResult); return propertyManager; } /** * Returns style sheets. * * @param root Root element. * @returns Style sheets. */ getStyleSheets(root) { if (!root) { return []; } const styleElements = (root.querySelectorAll('style,link[rel="stylesheet"]')); let styleSheets = []; for (const styleElement of styleElements) { const sheet = styleElement.sheet; if (sheet) { styleSheets.push(sheet); } } if (root.adoptedStyleSheets) { styleSheets = styleSheets.concat(root.adoptedStyleSheets); } return styleSheets; } /** * Applies CSS text to elements. * * @param options Options. * @param options.elements Elements. * @param options.cssRules CSS rules. * @param [options.hostElement] Host element. * @param [options.scopeElement] Scope element. */ parseCSSRules(options) { if (!options.hostElement && !options.elements.length) { return; } const window = this.element[PropertySymbol.window]; for (const rule of options.cssRules) { if (rule.type === CSSRuleTypeEnum.styleRule) { const selectorText = rule.selectorText; if (selectorText) { if (selectorText[0] === ':' && selectorText.startsWith(':host')) { if (options.hostElement) { let isTargetHost = true; if (selectorText !== ':host') { const selectorMatch = selectorText.match(HOST_REGEXP); if (selectorMatch) { const match = QuerySelector.matches(options.hostElement.element, selectorMatch[1] || selectorMatch[2], { ignoreErrors: true, scope: options.scopeElement?.element }); if (match) { const hostContextSelectorText = selectorText?.replace(HOST_REGEXP, '').trim(); if (hostContextSelectorText && hostContextSelectorText[0] !== ':') { isTargetHost = false; for (const element of options.elements) { const match = QuerySelector.matches(element.element, hostContextSelectorText, { ignoreErrors: true, scope: options.scopeElement?.element }); if (match) { element.cssTexts.push({ cssText: rule[PropertySymbol.cssText], priorityWeight: 10 + match.priorityWeight }); } } } } else { isTargetHost = false; } } } if (isTargetHost) { options.hostElement.cssTexts.push({ cssText: rule[PropertySymbol.cssText], priorityWeight: 10 }); } } } else { for (const element of options.elements) { const match = QuerySelector.matches(element.element, selectorText, { ignoreErrors: true, scope: options.scopeElement?.element }); if (match) { element.cssTexts.push({ cssText: rule[PropertySymbol.cssText], priorityWeight: match.priorityWeight }); } } } } } else if (rule.type === CSSRuleTypeEnum.mediaRule && // TODO: We need to send in a predefined root font size as it will otherwise be calculated using Window.getComputedStyle(), which will cause a never ending loop. Is there another solution? new MediaQueryList({ window, media: rule.conditionText, rootFontSize: this.element[PropertySymbol.tagName] === 'HTML' ? 16 : null }).matches) { this.parseCSSRules({ elements: options.elements, cssRules: rule.cssRules, hostElement: options.hostElement, scopeElement: options.scopeElement }); } else if (rule.type === CSSRuleTypeEnum.supportsRule) { if (window.CSS.supports(rule.conditionText)) { this.parseCSSRules({ elements: options.elements, cssRules: rule.cssRules, hostElement: options.hostElement, scopeElement: options.scopeElement }); } } else if (rule.type === CSSRuleTypeEnum.containerRule) { if (rule instanceof CSSScopeRule) { const scopedElements = []; let scope = null; for (const element of options.elements) { if (scope) { if (rule[PropertySymbol.end]) { const match = QuerySelector.matches(element.element, rule[PropertySymbol.end], { ignoreErrors: true, scope: scope.element }); if (match) { break; } } scopedElements.push(element); } else { const match = QuerySelector.matches(element.element, rule[PropertySymbol.start] || ':root', { ignoreErrors: true }); if (match) { scope = element; scopedElements.push(element); } } } if (scopedElements.length) { this.parseCSSRules({ elements: scopedElements, cssRules: rule.cssRules, hostElement: options.hostElement, scopeElement: scope }); } } // TODO: Add support for CSSContainerRule, which would require element sizes to be measured. } } } /** * Parses CSS variables in a value. * * @param value Value. * @param cssVariables CSS variables. * @returns CSS value. */ parseCSSVariablesInValue(value, cssVariables) { let newValue = value; let match; while ((match = newValue.match(SINGLE_CSS_VARIABLE_REGEXP)) != null) { // Without fallback value - E.g. var(--my-var) newValue = newValue.replace(match[0], cssVariables[match[1]] || ''); } while ((match = newValue.match(CSS_VARIABLE_REGEXP)) !== null) { // Fallback value - E.g. var(--my-var, #FFFFFF) newValue = newValue.replace(match[0], cssVariables[match[1]] || match[2]); } return newValue; } /** * Parses measurements in a value. * * @param options Options. * @param options.value Value. * @param options.rootFontSize Root font size. * @param options.parentFontSize Parent font size. * @param [options.parentSize] Parent width. * @returns CSS value. */ parseMeasurementsInValue(options) { if (new WindowBrowserContext(this.element[PropertySymbol.window]).getSettings() ?.disableComputedStyleRendering) { return options.value; } const regexp = new RegExp(CSS_MEASUREMENT_REGEXP); let newValue = options.value; let match; while ((match = regexp.exec(options.value)) !== null) { if (match[1] !== 'px') { const valueInPixels = CSSMeasurementConverter.toPixels({ window: this.element[PropertySymbol.window], value: match[0], rootFontSize: options.rootFontSize, parentFontSize: options.parentFontSize, parentSize: options.parentSize }); if (valueInPixels !== null) { newValue = newValue.replace(match[0], valueInPixels + 'px'); } } } return newValue; } } //# sourceMappingURL=CSSStyleDeclarationComputedStyle.js.map