/** @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 {nativeShadow} from './style-settings.js'; import StyleTransformer from './style-transformer.js'; import {getIsExtends, elementHasBuiltCss, wrap} from './style-util.js'; export let flush = function() {}; /** * @param {!Element} element * @return {string} */ function getClasses(element) { if (element.classList && element.classList.value) { return element.classList.value; } else { // NOTE: className is patched to remove scoping classes in ShadyDOM // use getAttribute('class') instead, which is unpatched return element.getAttribute('class') || ''; } } const scopeRegExp = new RegExp(`${StyleTransformer.SCOPE_NAME}\\s*([^\\s]*)`); /** * @param {!Element} element * @return {string} */ export function getCurrentScope(element) { const match = getClasses(element).match(scopeRegExp); if (match) { return match[1]; } else { return ''; } } /** * @param {!Node} node */ export function getOwnerScope(node) { const ownerRoot = wrap(node).getRootNode(); if (ownerRoot === node || ownerRoot === node.ownerDocument) { return ''; } const host = /** @type {!ShadowRoot} */(ownerRoot).host; if (!host) { // this may actually be a document fragment return ''; } return getIsExtends(host).is; } /** * @param {!Element} element */ export function ensureCorrectScope(element) { const currentScope = getCurrentScope(element); const ownerRoot = wrap(element).getRootNode(); if (ownerRoot === element) { return; } if (currentScope && ownerRoot === element.ownerDocument) { // node was scoped, but now is in document StyleTransformer.domRemoveScope(element, currentScope); } else if (ownerRoot instanceof ShadowRoot) { const ownerScope = getOwnerScope(element); if (ownerScope !== currentScope) { // node was scoped, but not by its current owner StyleTransformer.domReplaceScope(element, currentScope, ownerScope); } } } /** * @param {!HTMLElement|!HTMLDocument} element */ export function ensureCorrectSubtreeScoping(element) { // find unscoped subtree nodes const unscopedNodes = window['ShadyDOM']['nativeMethods']['querySelectorAll'].call( element, `:not(.${StyleTransformer.SCOPE_NAME})`); for (let j = 0; j < unscopedNodes.length; j++) { // it's possible, during large batch inserts, that nodes that aren't // scoped within the current scope were added. // To make sure that any unscoped nodes that were inserted in the current batch are correctly styled, // query all unscoped nodes and force their style-scope to be applied. // This could happen if a sub-element appended an unscoped node in its shadowroot and this function // runs on a parent element of the host of that unscoped node: // parent-element -> element -> unscoped node // Here unscoped node should have the style-scope element, not parent-element. const unscopedNode = unscopedNodes[j]; const scopeForPreviouslyUnscopedNode = getOwnerScope(unscopedNode); if (scopeForPreviouslyUnscopedNode) { StyleTransformer.element(unscopedNode, scopeForPreviouslyUnscopedNode); } } } /** * @param {HTMLElement} el * @return {boolean} */ function isElementWithBuiltCss(el) { if (el.localName === 'style' || el.localName === 'template') { return elementHasBuiltCss(el); } return false; } /** * @param {Array|null} mxns */ function handler(mxns) { for (let x=0; x < mxns.length; x++) { let mxn = mxns[x]; if (mxn.target === document.documentElement || mxn.target === document.head) { continue; } for (let i=0; i < mxn.addedNodes.length; i++) { let n = mxn.addedNodes[i]; if (n.nodeType !== Node.ELEMENT_NODE) { continue; } n = /** @type {HTMLElement} */(n); // eslint-disable-line no-self-assign let root = n.getRootNode(); let currentScope = getCurrentScope(n); // node was scoped, but now is in document // If this element has built css, we must not remove scoping as this node // will be used as a template or style without re - applying scoping as an optimization if (currentScope && root === n.ownerDocument && !isElementWithBuiltCss(n)) { StyleTransformer.domRemoveScope(n, currentScope); } else if (root instanceof ShadowRoot) { const newScope = getOwnerScope(n); // rescope current node and subtree if necessary if (newScope !== currentScope) { StyleTransformer.domReplaceScope(n, currentScope, newScope); } // make sure all the subtree elements are scoped correctly ensureCorrectSubtreeScoping(n); } } } } // if native Shadow DOM is being used, or ShadyDOM handles dynamic scoiping, do not activate the MutationObserver if (!nativeShadow && !(window['ShadyDOM'] && window['ShadyDOM']['handlesDynamicScoping'])) { let observer = new MutationObserver(handler); let start = (node) => { observer.observe(node, {childList: true, subtree: true}); } let nativeCustomElements = (window['customElements'] && !window['customElements']['polyfillWrapFlushCallback']); // need to start immediately with native custom elements // TODO(dfreedm): with polyfilled HTMLImports and native custom elements // excessive mutations may be observed; this can be optimized via cooperation // with the HTMLImports polyfill. if (nativeCustomElements) { start(document); } else { let delayedStart = () => { start(document.body); } // use polyfill timing if it's available if (window['HTMLImports']) { window['HTMLImports']['whenReady'](delayedStart); // otherwise push beyond native imports being ready // which requires RAF + readystate interactive. } else { requestAnimationFrame(function() { if (document.readyState === 'loading') { let listener = function() { delayedStart(); document.removeEventListener('readystatechange', listener); } document.addEventListener('readystatechange', listener); } else { delayedStart(); } }); } } flush = function() { handler(observer.takeRecords()); } }