blob: 9cf34f0541e8eff3e607eaf0ce396dfeb821d35f [file] [log] [blame]
/**
@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<MutationRecord|null>|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());
}
}