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