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