• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {nativeShadow} from './style-settings.js';
14import StyleTransformer from './style-transformer.js';
15import {getIsExtends, elementHasBuiltCss, wrap} from './style-util.js';
16
17export let flush = function() {};
18
19/**
20 * @param {!Element} element
21 * @return {string}
22 */
23function getClasses(element) {
24  if (element.classList && element.classList.value) {
25    return element.classList.value;
26  } else {
27    // NOTE: className is patched to remove scoping classes in ShadyDOM
28    // use getAttribute('class') instead, which is unpatched
29    return element.getAttribute('class') || '';
30  }
31}
32
33const scopeRegExp = new RegExp(`${StyleTransformer.SCOPE_NAME}\\s*([^\\s]*)`);
34
35/**
36 * @param {!Element} element
37 * @return {string}
38 */
39export function getCurrentScope(element) {
40  const match = getClasses(element).match(scopeRegExp);
41  if (match) {
42    return match[1];
43  } else {
44    return '';
45  }
46}
47
48/**
49 * @param {!Node} node
50 */
51export function getOwnerScope(node) {
52  const ownerRoot = wrap(node).getRootNode();
53  if (ownerRoot === node || ownerRoot === node.ownerDocument) {
54    return '';
55  }
56  const host = /** @type {!ShadowRoot} */(ownerRoot).host;
57  if (!host) {
58    // this may actually be a document fragment
59    return '';
60  }
61  return getIsExtends(host).is;
62}
63
64/**
65 * @param {!Element} element
66 */
67export function ensureCorrectScope(element) {
68  const currentScope = getCurrentScope(element);
69  const ownerRoot = wrap(element).getRootNode();
70  if (ownerRoot === element) {
71    return;
72  }
73  if (currentScope && ownerRoot === element.ownerDocument) {
74    // node was scoped, but now is in document
75    StyleTransformer.domRemoveScope(element, currentScope);
76  } else if (ownerRoot instanceof ShadowRoot) {
77    const ownerScope = getOwnerScope(element);
78    if (ownerScope !== currentScope) {
79      // node was scoped, but not by its current owner
80      StyleTransformer.domReplaceScope(element, currentScope, ownerScope);
81    }
82  }
83}
84
85/**
86 * @param {!HTMLElement|!HTMLDocument} element
87 */
88export function ensureCorrectSubtreeScoping(element) {
89  // find unscoped subtree nodes
90  const unscopedNodes = window['ShadyDOM']['nativeMethods']['querySelectorAll'].call(
91    element, `:not(.${StyleTransformer.SCOPE_NAME})`);
92
93  for (let j = 0; j < unscopedNodes.length; j++) {
94    // it's possible, during large batch inserts, that nodes that aren't
95    // scoped within the current scope were added.
96    // To make sure that any unscoped nodes that were inserted in the current batch are correctly styled,
97    // query all unscoped nodes and force their style-scope to be applied.
98    // This could happen if a sub-element appended an unscoped node in its shadowroot and this function
99    // runs on a parent element of the host of that unscoped node:
100    // parent-element -> element -> unscoped node
101    // Here unscoped node should have the style-scope element, not parent-element.
102    const unscopedNode = unscopedNodes[j];
103    const scopeForPreviouslyUnscopedNode = getOwnerScope(unscopedNode);
104    if (scopeForPreviouslyUnscopedNode) {
105      StyleTransformer.element(unscopedNode, scopeForPreviouslyUnscopedNode);
106    }
107  }
108}
109
110/**
111 * @param {HTMLElement} el
112 * @return {boolean}
113 */
114function isElementWithBuiltCss(el) {
115  if (el.localName === 'style' || el.localName === 'template') {
116    return elementHasBuiltCss(el);
117  }
118  return false;
119}
120
121/**
122 * @param {Array<MutationRecord|null>|null} mxns
123 */
124function handler(mxns) {
125  for (let x=0; x < mxns.length; x++) {
126    let mxn = mxns[x];
127    if (mxn.target === document.documentElement ||
128      mxn.target === document.head) {
129      continue;
130    }
131    for (let i=0; i < mxn.addedNodes.length; i++) {
132      let n = mxn.addedNodes[i];
133      if (n.nodeType !== Node.ELEMENT_NODE) {
134        continue;
135      }
136      n = /** @type {HTMLElement} */(n); // eslint-disable-line no-self-assign
137      let root = n.getRootNode();
138      let currentScope = getCurrentScope(n);
139      // node was scoped, but now is in document
140      // If this element has built css, we must not remove scoping as this node
141      // will be used as a template or style without re - applying scoping as an optimization
142      if (currentScope && root === n.ownerDocument && !isElementWithBuiltCss(n)) {
143        StyleTransformer.domRemoveScope(n, currentScope);
144      } else if (root instanceof ShadowRoot) {
145        const newScope = getOwnerScope(n);
146        // rescope current node and subtree if necessary
147        if (newScope !== currentScope) {
148          StyleTransformer.domReplaceScope(n, currentScope, newScope);
149        }
150        // make sure all the subtree elements are scoped correctly
151        ensureCorrectSubtreeScoping(n);
152      }
153    }
154  }
155}
156
157// if native Shadow DOM is being used, or ShadyDOM handles dynamic scoiping, do not activate the MutationObserver
158if (!nativeShadow && !(window['ShadyDOM'] && window['ShadyDOM']['handlesDynamicScoping'])) {
159  let observer = new MutationObserver(handler);
160  let start = (node) => {
161    observer.observe(node, {childList: true, subtree: true});
162  }
163  let nativeCustomElements = (window['customElements'] &&
164    !window['customElements']['polyfillWrapFlushCallback']);
165  // need to start immediately with native custom elements
166  // TODO(dfreedm): with polyfilled HTMLImports and native custom elements
167  // excessive mutations may be observed; this can be optimized via cooperation
168  // with the HTMLImports polyfill.
169  if (nativeCustomElements) {
170    start(document);
171  } else {
172    let delayedStart = () => {
173      start(document.body);
174    }
175    // use polyfill timing if it's available
176    if (window['HTMLImports']) {
177      window['HTMLImports']['whenReady'](delayedStart);
178    // otherwise push beyond native imports being ready
179    // which requires RAF + readystate interactive.
180    } else {
181      requestAnimationFrame(function() {
182        if (document.readyState === 'loading') {
183          let listener = function() {
184            delayedStart();
185            document.removeEventListener('readystatechange', listener);
186          }
187          document.addEventListener('readystatechange', listener);
188        } else {
189          delayedStart();
190        }
191      });
192    }
193  }
194
195  flush = function() {
196    handler(observer.takeRecords());
197  }
198}
199