1/** 2@license 3Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7Code distributed by Google as part of the polymer project is also 8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9*/ 10 11'use strict'; 12 13import {parse, StyleNode} from './css-parse.js'; 14import {nativeShadow, nativeCssVariables} from './style-settings.js'; 15import StyleTransformer from './style-transformer.js'; 16import * as StyleUtil from './style-util.js'; 17import StyleProperties from './style-properties.js'; 18import {ensureStylePlaceholder, getStylePlaceholder} from './style-placeholder.js'; 19import StyleInfo from './style-info.js'; 20import StyleCache from './style-cache.js'; 21import {flush as watcherFlush, getOwnerScope, getCurrentScope} from './document-watcher.js'; 22import templateMap from './template-map.js'; 23import * as ApplyShimUtils from './apply-shim-utils.js'; 24import {updateNativeProperties, detectMixin} from './common-utils.js'; 25import {CustomStyleInterfaceInterface} from './custom-style-interface.js'; // eslint-disable-line no-unused-vars 26 27/** 28 * @const {StyleCache} 29 */ 30const styleCache = new StyleCache(); 31 32export default class ScopingShim { 33 constructor() { 34 this._scopeCounter = {}; 35 this._documentOwner = /** @type {!HTMLElement} */(document.documentElement); 36 let ast = new StyleNode(); 37 ast['rules'] = []; 38 this._documentOwnerStyleInfo = StyleInfo.set(this._documentOwner, new StyleInfo(ast)); 39 this._elementsHaveApplied = false; 40 /** @type {?Object} */ 41 this._applyShim = null; 42 /** @type {?CustomStyleInterfaceInterface} */ 43 this._customStyleInterface = null; 44 } 45 flush() { 46 watcherFlush(); 47 } 48 _generateScopeSelector(name) { 49 let id = this._scopeCounter[name] = (this._scopeCounter[name] || 0) + 1; 50 return `${name}-${id}`; 51 } 52 getStyleAst(style) { 53 return StyleUtil.rulesForStyle(style); 54 } 55 styleAstToString(ast) { 56 return StyleUtil.toCssText(ast); 57 } 58 _gatherStyles(template) { 59 return StyleUtil.gatherStyleText(template.content); 60 } 61 /** 62 * Prepare the styling and template for the given element type 63 * 64 * @param {!HTMLTemplateElement} template 65 * @param {string} elementName 66 * @param {string=} typeExtension 67 */ 68 prepareTemplate(template, elementName, typeExtension) { 69 this.prepareTemplateDom(template, elementName); 70 this.prepareTemplateStyles(template, elementName, typeExtension); 71 } 72 /** 73 * Prepare styling for the given element type 74 * @param {!HTMLTemplateElement} template 75 * @param {string} elementName 76 * @param {string=} typeExtension 77 */ 78 prepareTemplateStyles(template, elementName, typeExtension) { 79 if (template._prepared) { 80 return; 81 } 82 // style placeholders are only used when ShadyDOM is active 83 if (!nativeShadow) { 84 ensureStylePlaceholder(elementName); 85 } 86 template._prepared = true; 87 template.name = elementName; 88 template.extends = typeExtension; 89 templateMap[elementName] = template; 90 let cssBuild = StyleUtil.getCssBuild(template); 91 const optimalBuild = StyleUtil.isOptimalCssBuild(cssBuild); 92 let info = { 93 is: elementName, 94 extends: typeExtension, 95 }; 96 let cssText = this._gatherStyles(template); 97 // check if the styling has mixin definitions or uses 98 this._ensure(); 99 if (!optimalBuild) { 100 let hasMixins = !cssBuild && detectMixin(cssText); 101 let ast = parse(cssText); 102 // only run the applyshim transforms if there is a mixin involved 103 if (hasMixins && nativeCssVariables && this._applyShim) { 104 this._applyShim['transformRules'](ast, elementName); 105 } 106 template['_styleAst'] = ast; 107 } 108 let ownPropertyNames = []; 109 if (!nativeCssVariables) { 110 ownPropertyNames = StyleProperties.decorateStyles(template['_styleAst']); 111 } 112 if (!ownPropertyNames.length || nativeCssVariables) { 113 let root = nativeShadow ? template.content : null; 114 let placeholder = getStylePlaceholder(elementName); 115 let style = this._generateStaticStyle(info, template['_styleAst'], root, placeholder, cssBuild, optimalBuild ? cssText : ''); 116 template._style = style; 117 } 118 template._ownPropertyNames = ownPropertyNames; 119 } 120 /** 121 * Prepare template for the given element type 122 * @param {!HTMLTemplateElement} template 123 * @param {string} elementName 124 */ 125 prepareTemplateDom(template, elementName) { 126 const cssBuild = StyleUtil.getCssBuild(template); 127 if (!nativeShadow && cssBuild !== 'shady' && !template._domPrepared) { 128 template._domPrepared = true; 129 StyleTransformer.domAddScope(template.content, elementName); 130 } 131 } 132 /** 133 * @param {!{is: string, extends: (string|undefined)}} info 134 * @param {!StyleNode} rules 135 * @param {DocumentFragment} shadowroot 136 * @param {Node} placeholder 137 * @param {string} cssBuild 138 * @param {string=} cssText 139 * @return {?HTMLStyleElement} 140 */ 141 _generateStaticStyle(info, rules, shadowroot, placeholder, cssBuild, cssText) { 142 cssText = StyleTransformer.elementStyles(info, rules, null, cssBuild, cssText); 143 if (cssText.length) { 144 return StyleUtil.applyCss(cssText, info.is, shadowroot, placeholder); 145 } 146 return null; 147 } 148 _prepareHost(host) { 149 const {is, typeExtension} = StyleUtil.getIsExtends(host); 150 const placeholder = getStylePlaceholder(is); 151 const template = templateMap[is]; 152 if (!template) { 153 return; 154 } 155 const ast = template['_styleAst']; 156 const ownStylePropertyNames = template._ownPropertyNames; 157 const cssBuild = StyleUtil.getCssBuild(template); 158 const styleInfo = new StyleInfo( 159 ast, 160 placeholder, 161 ownStylePropertyNames, 162 is, 163 typeExtension, 164 cssBuild 165 ); 166 StyleInfo.set(host, styleInfo); 167 return styleInfo; 168 } 169 _ensureApplyShim() { 170 if (this._applyShim) { 171 return; 172 } else if (window.ShadyCSS && window.ShadyCSS.ApplyShim) { 173 this._applyShim = /** @type {!Object} */ (window.ShadyCSS.ApplyShim); 174 this._applyShim['invalidCallback'] = ApplyShimUtils.invalidate; 175 } 176 } 177 _ensureCustomStyleInterface() { 178 if (this._customStyleInterface) { 179 return; 180 } else if (window.ShadyCSS && window.ShadyCSS.CustomStyleInterface) { 181 this._customStyleInterface = /** @type {!CustomStyleInterfaceInterface} */(window.ShadyCSS.CustomStyleInterface); 182 /** @type {function(!HTMLStyleElement)} */ 183 this._customStyleInterface['transformCallback'] = (style) => {this.transformCustomStyleForDocument(style)}; 184 this._customStyleInterface['validateCallback'] = () => { 185 requestAnimationFrame(() => { 186 if (this._customStyleInterface['enqueued'] || this._elementsHaveApplied) { 187 this.flushCustomStyles(); 188 } 189 }) 190 }; 191 } 192 } 193 _ensure() { 194 this._ensureApplyShim(); 195 this._ensureCustomStyleInterface(); 196 } 197 /** 198 * Flush and apply custom styles to document 199 */ 200 flushCustomStyles() { 201 this._ensure(); 202 if (!this._customStyleInterface) { 203 return; 204 } 205 let customStyles = this._customStyleInterface['processStyles'](); 206 // early return if custom-styles don't need validation 207 if (!this._customStyleInterface['enqueued']) { 208 return; 209 } 210 // bail if custom styles are built optimally 211 if (StyleUtil.isOptimalCssBuild(this._documentOwnerStyleInfo.cssBuild)) { 212 return; 213 } 214 if (!nativeCssVariables) { 215 this._updateProperties(this._documentOwner, this._documentOwnerStyleInfo); 216 this._applyCustomStyles(customStyles); 217 if (this._elementsHaveApplied) { 218 // if custom elements have upgraded and there are no native css variables, we must recalculate the whole tree 219 this.styleDocument(); 220 } 221 } else if (!this._documentOwnerStyleInfo.cssBuild) { 222 this._revalidateCustomStyleApplyShim(customStyles); 223 } 224 this._customStyleInterface['enqueued'] = false; 225 } 226 /** 227 * Apply styles for the given element 228 * 229 * @param {!HTMLElement} host 230 * @param {Object=} overrideProps 231 */ 232 styleElement(host, overrideProps) { 233 const styleInfo = StyleInfo.get(host) || this._prepareHost(host); 234 // if there is no style info at this point, bail 235 if (!styleInfo) { 236 return; 237 } 238 // Only trip the `elementsHaveApplied` flag if a node other that the root document has `applyStyle` called 239 if (!this._isRootOwner(host)) { 240 this._elementsHaveApplied = true; 241 } 242 if (overrideProps) { 243 styleInfo.overrideStyleProperties = 244 styleInfo.overrideStyleProperties || {}; 245 Object.assign(styleInfo.overrideStyleProperties, overrideProps); 246 } 247 if (!nativeCssVariables) { 248 this.styleElementShimVariables(host, styleInfo); 249 } else { 250 this.styleElementNativeVariables(host, styleInfo); 251 } 252 } 253 /** 254 * @param {!HTMLElement} host 255 * @param {!StyleInfo} styleInfo 256 */ 257 styleElementShimVariables(host, styleInfo) { 258 this.flush(); 259 this._updateProperties(host, styleInfo); 260 if (styleInfo.ownStylePropertyNames && styleInfo.ownStylePropertyNames.length) { 261 this._applyStyleProperties(host, styleInfo); 262 } 263 } 264 /** 265 * @param {!HTMLElement} host 266 * @param {!StyleInfo} styleInfo 267 */ 268 styleElementNativeVariables(host, styleInfo) { 269 const { is } = StyleUtil.getIsExtends(host); 270 if (styleInfo.overrideStyleProperties) { 271 updateNativeProperties(host, styleInfo.overrideStyleProperties); 272 } 273 const template = templateMap[is]; 274 // bail early if there is no shadowroot for this element 275 if (!template && !this._isRootOwner(host)) { 276 return; 277 } 278 // bail early if the template was built with polymer-css-build 279 if (template && StyleUtil.elementHasBuiltCss(template)) { 280 return; 281 } 282 if (template && template._style && !ApplyShimUtils.templateIsValid(template)) { 283 // update template 284 if (!ApplyShimUtils.templateIsValidating(template)) { 285 this._ensure(); 286 this._applyShim && this._applyShim['transformRules'](template['_styleAst'], is); 287 template._style.textContent = StyleTransformer.elementStyles(host, styleInfo.styleRules); 288 ApplyShimUtils.startValidatingTemplate(template); 289 } 290 // update instance if native shadowdom 291 if (nativeShadow) { 292 let root = host.shadowRoot; 293 if (root) { 294 let style = root.querySelector('style'); 295 if (style) { 296 style.textContent = StyleTransformer.elementStyles(host, styleInfo.styleRules); 297 } 298 } 299 } 300 styleInfo.styleRules = template['_styleAst']; 301 } 302 } 303 _styleOwnerForNode(node) { 304 let root = StyleUtil.wrap(node).getRootNode(); 305 let host = root.host; 306 if (host) { 307 if (StyleInfo.get(host) || this._prepareHost(host)) { 308 return host; 309 } else { 310 return this._styleOwnerForNode(host); 311 } 312 } 313 return this._documentOwner; 314 } 315 _isRootOwner(node) { 316 return (node === this._documentOwner); 317 } 318 _applyStyleProperties(host, styleInfo) { 319 let is = StyleUtil.getIsExtends(host).is; 320 let cacheEntry = styleCache.fetch(is, styleInfo.styleProperties, styleInfo.ownStylePropertyNames); 321 let cachedScopeSelector = cacheEntry && cacheEntry.scopeSelector; 322 let cachedStyle = cacheEntry ? cacheEntry.styleElement : null; 323 let oldScopeSelector = styleInfo.scopeSelector; 324 // only generate new scope if cached style is not found 325 styleInfo.scopeSelector = cachedScopeSelector || this._generateScopeSelector(is); 326 let style = StyleProperties.applyElementStyle(host, styleInfo.styleProperties, styleInfo.scopeSelector, cachedStyle); 327 if (!nativeShadow) { 328 StyleProperties.applyElementScopeSelector(host, styleInfo.scopeSelector, oldScopeSelector); 329 } 330 if (!cacheEntry) { 331 styleCache.store(is, styleInfo.styleProperties, style, styleInfo.scopeSelector); 332 } 333 return style; 334 } 335 _updateProperties(host, styleInfo) { 336 let owner = this._styleOwnerForNode(host); 337 let ownerStyleInfo = StyleInfo.get(owner); 338 let ownerProperties = ownerStyleInfo.styleProperties; 339 // style owner has not updated properties yet 340 // go up the chain and force property update, 341 // except if the owner is the document 342 if (owner !== this._documentOwner && !ownerProperties) { 343 this._updateProperties(owner, ownerStyleInfo); 344 ownerProperties = ownerStyleInfo.styleProperties; 345 } 346 let props = Object.create(ownerProperties || null); 347 let hostAndRootProps = StyleProperties.hostAndRootPropertiesForScope(host, styleInfo.styleRules, styleInfo.cssBuild); 348 let propertyData = StyleProperties.propertyDataFromStyles(ownerStyleInfo.styleRules, host); 349 let propertiesMatchingHost = propertyData.properties 350 Object.assign( 351 props, 352 hostAndRootProps.hostProps, 353 propertiesMatchingHost, 354 hostAndRootProps.rootProps 355 ); 356 this._mixinOverrideStyles(props, styleInfo.overrideStyleProperties); 357 StyleProperties.reify(props); 358 styleInfo.styleProperties = props; 359 } 360 _mixinOverrideStyles(props, overrides) { 361 for (let p in overrides) { 362 let v = overrides[p]; 363 // skip override props if they are not truthy or 0 364 // in order to fall back to inherited values 365 if (v || v === 0) { 366 props[p] = v; 367 } 368 } 369 } 370 /** 371 * Update styles of the whole document 372 * 373 * @param {Object=} properties 374 */ 375 styleDocument(properties) { 376 this.styleSubtree(this._documentOwner, properties); 377 } 378 /** 379 * Update styles of a subtree 380 * 381 * @param {!HTMLElement} host 382 * @param {Object=} properties 383 */ 384 styleSubtree(host, properties) { 385 let root = host.shadowRoot; 386 if (root || this._isRootOwner(host)) { 387 this.styleElement(host, properties); 388 } 389 // process the shadowdom children of `host` 390 let shadowChildren = 391 root && (/** @type {!ParentNode} */ (root).children || root.childNodes); 392 if (shadowChildren) { 393 for (let i = 0; i < shadowChildren.length; i++) { 394 let c = /** @type {!HTMLElement} */(shadowChildren[i]); 395 this.styleSubtree(c); 396 } 397 } else { 398 // process the lightdom children of `host` 399 let children = host.children || host.childNodes; 400 if (children) { 401 for (let i = 0; i < children.length; i++) { 402 let c = /** @type {!HTMLElement} */(children[i]); 403 this.styleSubtree(c); 404 } 405 } 406 } 407 } 408 /* Custom Style operations */ 409 _revalidateCustomStyleApplyShim(customStyles) { 410 for (let i = 0; i < customStyles.length; i++) { 411 let c = customStyles[i]; 412 let s = this._customStyleInterface['getStyleForCustomStyle'](c); 413 if (s) { 414 this._revalidateApplyShim(s); 415 } 416 } 417 } 418 _applyCustomStyles(customStyles) { 419 for (let i = 0; i < customStyles.length; i++) { 420 let c = customStyles[i]; 421 let s = this._customStyleInterface['getStyleForCustomStyle'](c); 422 if (s) { 423 StyleProperties.applyCustomStyle(s, this._documentOwnerStyleInfo.styleProperties); 424 } 425 } 426 } 427 transformCustomStyleForDocument(style) { 428 const cssBuild = StyleUtil.getCssBuild(style); 429 if (cssBuild !== this._documentOwnerStyleInfo.cssBuild) { 430 this._documentOwnerStyleInfo.cssBuild = cssBuild; 431 } 432 if (StyleUtil.isOptimalCssBuild(cssBuild)) { 433 return; 434 } 435 let ast = StyleUtil.rulesForStyle(style); 436 StyleUtil.forEachRule(ast, (rule) => { 437 if (nativeShadow) { 438 StyleTransformer.normalizeRootSelector(rule); 439 } else { 440 StyleTransformer.documentRule(rule); 441 } 442 if (nativeCssVariables && cssBuild === '') { 443 this._ensure(); 444 this._applyShim && this._applyShim['transformRule'](rule); 445 } 446 }); 447 if (nativeCssVariables) { 448 style.textContent = StyleUtil.toCssText(ast); 449 } else { 450 this._documentOwnerStyleInfo.styleRules['rules'].push(ast); 451 } 452 } 453 _revalidateApplyShim(style) { 454 if (nativeCssVariables && this._applyShim) { 455 let ast = StyleUtil.rulesForStyle(style); 456 this._ensure(); 457 this._applyShim['transformRules'](ast); 458 style.textContent = StyleUtil.toCssText(ast); 459 } 460 } 461 getComputedStyleValue(element, property) { 462 let value; 463 if (!nativeCssVariables) { 464 // element is either a style host, or an ancestor of a style host 465 let styleInfo = StyleInfo.get(element) || StyleInfo.get(this._styleOwnerForNode(element)); 466 value = styleInfo.styleProperties[property]; 467 } 468 // fall back to the property value from the computed styling 469 value = value || window.getComputedStyle(element).getPropertyValue(property); 470 // trim whitespace that can come after the `:` in css 471 // example: padding: 2px -> " 2px" 472 return value ? value.trim() : ''; 473 } 474 // given an element and a classString, replaces 475 // the element's class with the provided classString and adds 476 // any necessary ShadyCSS static and property based scoping selectors 477 setElementClass(element, classString) { 478 let root = StyleUtil.wrap(element).getRootNode(); 479 let classes = classString ? classString.split(/\s/) : []; 480 let scopeName = root.host && root.host.localName; 481 // If no scope, try to discover scope name from existing class. 482 // This can occur if, for example, a template stamped element that 483 // has been scoped is manipulated when not in a root. 484 if (!scopeName) { 485 var classAttr = element.getAttribute('class'); 486 if (classAttr) { 487 let k$ = classAttr.split(/\s/); 488 for (let i=0; i < k$.length; i++) { 489 if (k$[i] === StyleTransformer.SCOPE_NAME) { 490 scopeName = k$[i+1]; 491 break; 492 } 493 } 494 } 495 } 496 if (scopeName) { 497 classes.push(StyleTransformer.SCOPE_NAME, scopeName); 498 } 499 if (!nativeCssVariables) { 500 let styleInfo = StyleInfo.get(element); 501 if (styleInfo && styleInfo.scopeSelector) { 502 classes.push(StyleProperties.XSCOPE_NAME, styleInfo.scopeSelector); 503 } 504 } 505 StyleUtil.setElementClassRaw(element, classes.join(' ')); 506 } 507 _styleInfoForNode(node) { 508 return StyleInfo.get(node); 509 } 510 /** 511 * @param {!Element} node 512 * @param {string} scope 513 */ 514 scopeNode(node, scope) { 515 StyleTransformer.element(node, scope); 516 } 517 /** 518 * @param {!Element} node 519 * @param {string} scope 520 */ 521 unscopeNode(node, scope) { 522 StyleTransformer.element(node, scope, true); 523 } 524 /** 525 * @param {!Node} node 526 * @return {string} 527 */ 528 scopeForNode(node) { 529 return getOwnerScope(node); 530 } 531 /** 532 * @param {!Element} node 533 * @return {string} 534 */ 535 currentScopeForNode(node) { 536 return getCurrentScope(node); 537 } 538} 539 540/* exports */ 541/* eslint-disable no-self-assign */ 542ScopingShim.prototype['flush'] = ScopingShim.prototype.flush; 543ScopingShim.prototype['prepareTemplate'] = ScopingShim.prototype.prepareTemplate; 544ScopingShim.prototype['styleElement'] = ScopingShim.prototype.styleElement; 545ScopingShim.prototype['styleDocument'] = ScopingShim.prototype.styleDocument; 546ScopingShim.prototype['styleSubtree'] = ScopingShim.prototype.styleSubtree; 547ScopingShim.prototype['getComputedStyleValue'] = ScopingShim.prototype.getComputedStyleValue; 548ScopingShim.prototype['setElementClass'] = ScopingShim.prototype.setElementClass; 549ScopingShim.prototype['_styleInfoForNode'] = ScopingShim.prototype._styleInfoForNode; 550ScopingShim.prototype['transformCustomStyleForDocument'] = ScopingShim.prototype.transformCustomStyleForDocument; 551ScopingShim.prototype['getStyleAst'] = ScopingShim.prototype.getStyleAst; 552ScopingShim.prototype['styleAstToString'] = ScopingShim.prototype.styleAstToString; 553ScopingShim.prototype['flushCustomStyles'] = ScopingShim.prototype.flushCustomStyles; 554ScopingShim.prototype['scopeNode'] = ScopingShim.prototype.scopeNode; 555ScopingShim.prototype['unscopeNode'] = ScopingShim.prototype.unscopeNode; 556ScopingShim.prototype['scopeForNode'] = ScopingShim.prototype.scopeForNode; 557ScopingShim.prototype['currentScopeForNode'] = ScopingShim.prototype.currentScopeForNode; 558/* eslint-enable no-self-assign */ 559Object.defineProperties(ScopingShim.prototype, { 560 'nativeShadow': { 561 get() { 562 return nativeShadow; 563 } 564 }, 565 'nativeCss': { 566 get() { 567 return nativeCssVariables; 568 } 569 } 570}); 571