/** @license Copyright (c) 2017 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ 'use strict'; import {parse, StyleNode} from './css-parse.js'; import {nativeShadow, nativeCssVariables} from './style-settings.js'; import StyleTransformer from './style-transformer.js'; import * as StyleUtil from './style-util.js'; import StyleProperties from './style-properties.js'; import {ensureStylePlaceholder, getStylePlaceholder} from './style-placeholder.js'; import StyleInfo from './style-info.js'; import StyleCache from './style-cache.js'; import {flush as watcherFlush, getOwnerScope, getCurrentScope} from './document-watcher.js'; import templateMap from './template-map.js'; import * as ApplyShimUtils from './apply-shim-utils.js'; import {updateNativeProperties, detectMixin} from './common-utils.js'; import {CustomStyleInterfaceInterface} from './custom-style-interface.js'; // eslint-disable-line no-unused-vars /** * @const {StyleCache} */ const styleCache = new StyleCache(); export default class ScopingShim { constructor() { this._scopeCounter = {}; this._documentOwner = /** @type {!HTMLElement} */(document.documentElement); let ast = new StyleNode(); ast['rules'] = []; this._documentOwnerStyleInfo = StyleInfo.set(this._documentOwner, new StyleInfo(ast)); this._elementsHaveApplied = false; /** @type {?Object} */ this._applyShim = null; /** @type {?CustomStyleInterfaceInterface} */ this._customStyleInterface = null; } flush() { watcherFlush(); } _generateScopeSelector(name) { let id = this._scopeCounter[name] = (this._scopeCounter[name] || 0) + 1; return `${name}-${id}`; } getStyleAst(style) { return StyleUtil.rulesForStyle(style); } styleAstToString(ast) { return StyleUtil.toCssText(ast); } _gatherStyles(template) { return StyleUtil.gatherStyleText(template.content); } /** * Prepare the styling and template for the given element type * * @param {!HTMLTemplateElement} template * @param {string} elementName * @param {string=} typeExtension */ prepareTemplate(template, elementName, typeExtension) { this.prepareTemplateDom(template, elementName); this.prepareTemplateStyles(template, elementName, typeExtension); } /** * Prepare styling for the given element type * @param {!HTMLTemplateElement} template * @param {string} elementName * @param {string=} typeExtension */ prepareTemplateStyles(template, elementName, typeExtension) { if (template._prepared) { return; } // style placeholders are only used when ShadyDOM is active if (!nativeShadow) { ensureStylePlaceholder(elementName); } template._prepared = true; template.name = elementName; template.extends = typeExtension; templateMap[elementName] = template; let cssBuild = StyleUtil.getCssBuild(template); const optimalBuild = StyleUtil.isOptimalCssBuild(cssBuild); let info = { is: elementName, extends: typeExtension, }; let cssText = this._gatherStyles(template); // check if the styling has mixin definitions or uses this._ensure(); if (!optimalBuild) { let hasMixins = !cssBuild && detectMixin(cssText); let ast = parse(cssText); // only run the applyshim transforms if there is a mixin involved if (hasMixins && nativeCssVariables && this._applyShim) { this._applyShim['transformRules'](ast, elementName); } template['_styleAst'] = ast; } let ownPropertyNames = []; if (!nativeCssVariables) { ownPropertyNames = StyleProperties.decorateStyles(template['_styleAst']); } if (!ownPropertyNames.length || nativeCssVariables) { let root = nativeShadow ? template.content : null; let placeholder = getStylePlaceholder(elementName); let style = this._generateStaticStyle(info, template['_styleAst'], root, placeholder, cssBuild, optimalBuild ? cssText : ''); template._style = style; } template._ownPropertyNames = ownPropertyNames; } /** * Prepare template for the given element type * @param {!HTMLTemplateElement} template * @param {string} elementName */ prepareTemplateDom(template, elementName) { const cssBuild = StyleUtil.getCssBuild(template); if (!nativeShadow && cssBuild !== 'shady' && !template._domPrepared) { template._domPrepared = true; StyleTransformer.domAddScope(template.content, elementName); } } /** * @param {!{is: string, extends: (string|undefined)}} info * @param {!StyleNode} rules * @param {DocumentFragment} shadowroot * @param {Node} placeholder * @param {string} cssBuild * @param {string=} cssText * @return {?HTMLStyleElement} */ _generateStaticStyle(info, rules, shadowroot, placeholder, cssBuild, cssText) { cssText = StyleTransformer.elementStyles(info, rules, null, cssBuild, cssText); if (cssText.length) { return StyleUtil.applyCss(cssText, info.is, shadowroot, placeholder); } return null; } _prepareHost(host) { const {is, typeExtension} = StyleUtil.getIsExtends(host); const placeholder = getStylePlaceholder(is); const template = templateMap[is]; if (!template) { return; } const ast = template['_styleAst']; const ownStylePropertyNames = template._ownPropertyNames; const cssBuild = StyleUtil.getCssBuild(template); const styleInfo = new StyleInfo( ast, placeholder, ownStylePropertyNames, is, typeExtension, cssBuild ); StyleInfo.set(host, styleInfo); return styleInfo; } _ensureApplyShim() { if (this._applyShim) { return; } else if (window.ShadyCSS && window.ShadyCSS.ApplyShim) { this._applyShim = /** @type {!Object} */ (window.ShadyCSS.ApplyShim); this._applyShim['invalidCallback'] = ApplyShimUtils.invalidate; } } _ensureCustomStyleInterface() { if (this._customStyleInterface) { return; } else if (window.ShadyCSS && window.ShadyCSS.CustomStyleInterface) { this._customStyleInterface = /** @type {!CustomStyleInterfaceInterface} */(window.ShadyCSS.CustomStyleInterface); /** @type {function(!HTMLStyleElement)} */ this._customStyleInterface['transformCallback'] = (style) => {this.transformCustomStyleForDocument(style)}; this._customStyleInterface['validateCallback'] = () => { requestAnimationFrame(() => { if (this._customStyleInterface['enqueued'] || this._elementsHaveApplied) { this.flushCustomStyles(); } }) }; } } _ensure() { this._ensureApplyShim(); this._ensureCustomStyleInterface(); } /** * Flush and apply custom styles to document */ flushCustomStyles() { this._ensure(); if (!this._customStyleInterface) { return; } let customStyles = this._customStyleInterface['processStyles'](); // early return if custom-styles don't need validation if (!this._customStyleInterface['enqueued']) { return; } // bail if custom styles are built optimally if (StyleUtil.isOptimalCssBuild(this._documentOwnerStyleInfo.cssBuild)) { return; } if (!nativeCssVariables) { this._updateProperties(this._documentOwner, this._documentOwnerStyleInfo); this._applyCustomStyles(customStyles); if (this._elementsHaveApplied) { // if custom elements have upgraded and there are no native css variables, we must recalculate the whole tree this.styleDocument(); } } else if (!this._documentOwnerStyleInfo.cssBuild) { this._revalidateCustomStyleApplyShim(customStyles); } this._customStyleInterface['enqueued'] = false; } /** * Apply styles for the given element * * @param {!HTMLElement} host * @param {Object=} overrideProps */ styleElement(host, overrideProps) { const styleInfo = StyleInfo.get(host) || this._prepareHost(host); // if there is no style info at this point, bail if (!styleInfo) { return; } // Only trip the `elementsHaveApplied` flag if a node other that the root document has `applyStyle` called if (!this._isRootOwner(host)) { this._elementsHaveApplied = true; } if (overrideProps) { styleInfo.overrideStyleProperties = styleInfo.overrideStyleProperties || {}; Object.assign(styleInfo.overrideStyleProperties, overrideProps); } if (!nativeCssVariables) { this.styleElementShimVariables(host, styleInfo); } else { this.styleElementNativeVariables(host, styleInfo); } } /** * @param {!HTMLElement} host * @param {!StyleInfo} styleInfo */ styleElementShimVariables(host, styleInfo) { this.flush(); this._updateProperties(host, styleInfo); if (styleInfo.ownStylePropertyNames && styleInfo.ownStylePropertyNames.length) { this._applyStyleProperties(host, styleInfo); } } /** * @param {!HTMLElement} host * @param {!StyleInfo} styleInfo */ styleElementNativeVariables(host, styleInfo) { const { is } = StyleUtil.getIsExtends(host); if (styleInfo.overrideStyleProperties) { updateNativeProperties(host, styleInfo.overrideStyleProperties); } const template = templateMap[is]; // bail early if there is no shadowroot for this element if (!template && !this._isRootOwner(host)) { return; } // bail early if the template was built with polymer-css-build if (template && StyleUtil.elementHasBuiltCss(template)) { return; } if (template && template._style && !ApplyShimUtils.templateIsValid(template)) { // update template if (!ApplyShimUtils.templateIsValidating(template)) { this._ensure(); this._applyShim && this._applyShim['transformRules'](template['_styleAst'], is); template._style.textContent = StyleTransformer.elementStyles(host, styleInfo.styleRules); ApplyShimUtils.startValidatingTemplate(template); } // update instance if native shadowdom if (nativeShadow) { let root = host.shadowRoot; if (root) { let style = root.querySelector('style'); if (style) { style.textContent = StyleTransformer.elementStyles(host, styleInfo.styleRules); } } } styleInfo.styleRules = template['_styleAst']; } } _styleOwnerForNode(node) { let root = StyleUtil.wrap(node).getRootNode(); let host = root.host; if (host) { if (StyleInfo.get(host) || this._prepareHost(host)) { return host; } else { return this._styleOwnerForNode(host); } } return this._documentOwner; } _isRootOwner(node) { return (node === this._documentOwner); } _applyStyleProperties(host, styleInfo) { let is = StyleUtil.getIsExtends(host).is; let cacheEntry = styleCache.fetch(is, styleInfo.styleProperties, styleInfo.ownStylePropertyNames); let cachedScopeSelector = cacheEntry && cacheEntry.scopeSelector; let cachedStyle = cacheEntry ? cacheEntry.styleElement : null; let oldScopeSelector = styleInfo.scopeSelector; // only generate new scope if cached style is not found styleInfo.scopeSelector = cachedScopeSelector || this._generateScopeSelector(is); let style = StyleProperties.applyElementStyle(host, styleInfo.styleProperties, styleInfo.scopeSelector, cachedStyle); if (!nativeShadow) { StyleProperties.applyElementScopeSelector(host, styleInfo.scopeSelector, oldScopeSelector); } if (!cacheEntry) { styleCache.store(is, styleInfo.styleProperties, style, styleInfo.scopeSelector); } return style; } _updateProperties(host, styleInfo) { let owner = this._styleOwnerForNode(host); let ownerStyleInfo = StyleInfo.get(owner); let ownerProperties = ownerStyleInfo.styleProperties; // style owner has not updated properties yet // go up the chain and force property update, // except if the owner is the document if (owner !== this._documentOwner && !ownerProperties) { this._updateProperties(owner, ownerStyleInfo); ownerProperties = ownerStyleInfo.styleProperties; } let props = Object.create(ownerProperties || null); let hostAndRootProps = StyleProperties.hostAndRootPropertiesForScope(host, styleInfo.styleRules, styleInfo.cssBuild); let propertyData = StyleProperties.propertyDataFromStyles(ownerStyleInfo.styleRules, host); let propertiesMatchingHost = propertyData.properties Object.assign( props, hostAndRootProps.hostProps, propertiesMatchingHost, hostAndRootProps.rootProps ); this._mixinOverrideStyles(props, styleInfo.overrideStyleProperties); StyleProperties.reify(props); styleInfo.styleProperties = props; } _mixinOverrideStyles(props, overrides) { for (let p in overrides) { let v = overrides[p]; // skip override props if they are not truthy or 0 // in order to fall back to inherited values if (v || v === 0) { props[p] = v; } } } /** * Update styles of the whole document * * @param {Object=} properties */ styleDocument(properties) { this.styleSubtree(this._documentOwner, properties); } /** * Update styles of a subtree * * @param {!HTMLElement} host * @param {Object=} properties */ styleSubtree(host, properties) { let root = host.shadowRoot; if (root || this._isRootOwner(host)) { this.styleElement(host, properties); } // process the shadowdom children of `host` let shadowChildren = root && (/** @type {!ParentNode} */ (root).children || root.childNodes); if (shadowChildren) { for (let i = 0; i < shadowChildren.length; i++) { let c = /** @type {!HTMLElement} */(shadowChildren[i]); this.styleSubtree(c); } } else { // process the lightdom children of `host` let children = host.children || host.childNodes; if (children) { for (let i = 0; i < children.length; i++) { let c = /** @type {!HTMLElement} */(children[i]); this.styleSubtree(c); } } } } /* Custom Style operations */ _revalidateCustomStyleApplyShim(customStyles) { for (let i = 0; i < customStyles.length; i++) { let c = customStyles[i]; let s = this._customStyleInterface['getStyleForCustomStyle'](c); if (s) { this._revalidateApplyShim(s); } } } _applyCustomStyles(customStyles) { for (let i = 0; i < customStyles.length; i++) { let c = customStyles[i]; let s = this._customStyleInterface['getStyleForCustomStyle'](c); if (s) { StyleProperties.applyCustomStyle(s, this._documentOwnerStyleInfo.styleProperties); } } } transformCustomStyleForDocument(style) { const cssBuild = StyleUtil.getCssBuild(style); if (cssBuild !== this._documentOwnerStyleInfo.cssBuild) { this._documentOwnerStyleInfo.cssBuild = cssBuild; } if (StyleUtil.isOptimalCssBuild(cssBuild)) { return; } let ast = StyleUtil.rulesForStyle(style); StyleUtil.forEachRule(ast, (rule) => { if (nativeShadow) { StyleTransformer.normalizeRootSelector(rule); } else { StyleTransformer.documentRule(rule); } if (nativeCssVariables && cssBuild === '') { this._ensure(); this._applyShim && this._applyShim['transformRule'](rule); } }); if (nativeCssVariables) { style.textContent = StyleUtil.toCssText(ast); } else { this._documentOwnerStyleInfo.styleRules['rules'].push(ast); } } _revalidateApplyShim(style) { if (nativeCssVariables && this._applyShim) { let ast = StyleUtil.rulesForStyle(style); this._ensure(); this._applyShim['transformRules'](ast); style.textContent = StyleUtil.toCssText(ast); } } getComputedStyleValue(element, property) { let value; if (!nativeCssVariables) { // element is either a style host, or an ancestor of a style host let styleInfo = StyleInfo.get(element) || StyleInfo.get(this._styleOwnerForNode(element)); value = styleInfo.styleProperties[property]; } // fall back to the property value from the computed styling value = value || window.getComputedStyle(element).getPropertyValue(property); // trim whitespace that can come after the `:` in css // example: padding: 2px -> " 2px" return value ? value.trim() : ''; } // given an element and a classString, replaces // the element's class with the provided classString and adds // any necessary ShadyCSS static and property based scoping selectors setElementClass(element, classString) { let root = StyleUtil.wrap(element).getRootNode(); let classes = classString ? classString.split(/\s/) : []; let scopeName = root.host && root.host.localName; // If no scope, try to discover scope name from existing class. // This can occur if, for example, a template stamped element that // has been scoped is manipulated when not in a root. if (!scopeName) { var classAttr = element.getAttribute('class'); if (classAttr) { let k$ = classAttr.split(/\s/); for (let i=0; i < k$.length; i++) { if (k$[i] === StyleTransformer.SCOPE_NAME) { scopeName = k$[i+1]; break; } } } } if (scopeName) { classes.push(StyleTransformer.SCOPE_NAME, scopeName); } if (!nativeCssVariables) { let styleInfo = StyleInfo.get(element); if (styleInfo && styleInfo.scopeSelector) { classes.push(StyleProperties.XSCOPE_NAME, styleInfo.scopeSelector); } } StyleUtil.setElementClassRaw(element, classes.join(' ')); } _styleInfoForNode(node) { return StyleInfo.get(node); } /** * @param {!Element} node * @param {string} scope */ scopeNode(node, scope) { StyleTransformer.element(node, scope); } /** * @param {!Element} node * @param {string} scope */ unscopeNode(node, scope) { StyleTransformer.element(node, scope, true); } /** * @param {!Node} node * @return {string} */ scopeForNode(node) { return getOwnerScope(node); } /** * @param {!Element} node * @return {string} */ currentScopeForNode(node) { return getCurrentScope(node); } } /* exports */ /* eslint-disable no-self-assign */ ScopingShim.prototype['flush'] = ScopingShim.prototype.flush; ScopingShim.prototype['prepareTemplate'] = ScopingShim.prototype.prepareTemplate; ScopingShim.prototype['styleElement'] = ScopingShim.prototype.styleElement; ScopingShim.prototype['styleDocument'] = ScopingShim.prototype.styleDocument; ScopingShim.prototype['styleSubtree'] = ScopingShim.prototype.styleSubtree; ScopingShim.prototype['getComputedStyleValue'] = ScopingShim.prototype.getComputedStyleValue; ScopingShim.prototype['setElementClass'] = ScopingShim.prototype.setElementClass; ScopingShim.prototype['_styleInfoForNode'] = ScopingShim.prototype._styleInfoForNode; ScopingShim.prototype['transformCustomStyleForDocument'] = ScopingShim.prototype.transformCustomStyleForDocument; ScopingShim.prototype['getStyleAst'] = ScopingShim.prototype.getStyleAst; ScopingShim.prototype['styleAstToString'] = ScopingShim.prototype.styleAstToString; ScopingShim.prototype['flushCustomStyles'] = ScopingShim.prototype.flushCustomStyles; ScopingShim.prototype['scopeNode'] = ScopingShim.prototype.scopeNode; ScopingShim.prototype['unscopeNode'] = ScopingShim.prototype.unscopeNode; ScopingShim.prototype['scopeForNode'] = ScopingShim.prototype.scopeForNode; ScopingShim.prototype['currentScopeForNode'] = ScopingShim.prototype.currentScopeForNode; /* eslint-enable no-self-assign */ Object.defineProperties(ScopingShim.prototype, { 'nativeShadow': { get() { return nativeShadow; } }, 'nativeCss': { get() { return nativeCssVariables; } } });